WPF MVVM есть ли альтернатива IValueConverter

#c# #wpf #mvvm

#c# #wpf #mvvm

Вопрос:

У меня есть несколько групп наблюдаемых коллекций элементов, которые обновляются в фоновом потоке с регулярным интервалом (от 30 до 60 секунд). Эти коллекции отображаются через ItemsControls в представлении. Родительский элемент и элементы в элементе управления имеют несколько атрибутов отображения, привязанных к статусу каждого элемента.

  1. Статус будет определять, какая фигура будет отображаться рядом с текстом, а также цвет обводки и заливки этой фигуры.
  2. Статус будет определять цвет фона и цвет текста текста для этого элемента.
  3. Статус будет определять, отображается ли таймер обратного отсчета в элементе (таймер не привязан к viewmodel)
  4. Статус может определять цвет границы родительского контейнера.

В настоящее время я выполняю эту логику в отдельных IValueConverters для каждого свойства. Это работает, но кажется громоздким и разбросанным. Я почти хочу каким-то образом подписаться на событие PropertyChanged в пользовательском интерфейсе и заставить его вызвать один метод для отображения всего отображения для этого элемента, чтобы вся логика содержалась в одном месте. Есть ли лучший способ сделать это или я должен просто придерживаться IValueConverters?

Вот пример того, что у меня есть.

Коллекция:

 public ObservableCollection<PanelItem> PanelItems1 
{
    get { return panelItems1; }
    set
    {
        panelItems1 = value;
        base.OnPropertyChanged("PanelItems1");
    }
}
 

PanelItem — это небольшая коллекция свойств, которые включают в себя: имя, значение (статус), Описание. ItemsControls аналогичен следующему:

 <Border Grid.Row="0" Grid.Column="0"
        BorderBrush="{Binding Path=PanelGroup1.HighestStatus, Converter={StaticResource ParentBorderColorConverter}}"
        BorderThickness="3" Height="Auto" Width="Auto" Margin="0,0,2,0">
    <StackPanel Grid.Column="0" Grid.Row="0">
        <Label Grid.Row="0" Grid.Column="0" Content="First Group" Style="{StaticResource panelTitle}" />
        <ItemsControl ItemsSource="{Binding Path=PanelItems1, Mode=TwoWay}"
                      ItemTemplate="{StaticResource PanelItemTemplate}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel IsItemsHost="True" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>

        <!-- Here is the Data Template -->
        <DataTemplate x:Key="PanelItemTemplate">
            <Viewbox MaxHeight="20" HorizontalAlignment="Left" Cursor="Hand">
                <WrapPanel>
                    <Path Margin="2,2,2,2" StrokeThickness="2">
                        <Path.Data>
                            <Binding Path="Status" Mode="OneWay" Converter="{StaticResource ShapeConverter}" />
                        </Path.Data>
                        <Path.Fill>
                            <Binding Path="Status" Mode="OneWay" Converter="{StaticResource ShapeColorConverter}" />
                        </Path.Fill>
                        <Path.Stroke>
                            <Binding Path="Status" Mode="OneWay" Converter="{StaticResource ShapeBorderConverter}" />
                        </Path.Stroke>
                    </Path>
                    <ContentPresenter Content="{Binding Path=Name}" Margin="5,0,0,0" />
                </WrapPanel>
            </Viewbox>
        </DataTemplate>
    </StackPanel>
</Border>
 

Комментарии:

1. Рассматривали ли вы возможность использования диспетчера визуальных состояний? Вы можете создать свой собственный шаблон управления с помощью VSM, создавая резервные копии любых визуальных состояний — очевидно, ему нужно знать, к чему он привязан, чтобы он мог применять эти состояния условно — вы также можете просто использовать триггеры вместо преобразователей значений

2. Вместо того, чтобы предоставлять один статус и применять 3 разных конвертера, вы можете предоставить 3 разных свойства на своей виртуальной машине и выполнить 3 преобразования, чтобы присвоить им значение в виртуальной машине (или в логике, вызываемой виртуальной машиной)

3. Проверьте здесь информацию о VSM blogs.msdn.com/b/wpfsdk/archive/2009/02/27 / … — существует также соответствующая информация о триггерах — я всегда рассматриваю преобразователи значений как способ преобразования значений, а не для запуска визуального состояния, поэтому я стараюсь избегать их на чем-либо громоздком, поскольку они в конечном итоге являются кодом, а не разметкой

4. @Charleh Я этого раньше не видел, очень интересно. Я читаю об этом прямо сейчас. Похоже, что он больше используется для событий взаимодействия с пользовательским интерфейсом, но я мог бы привязать его к свойству viewmodel. Похоже, мне придется добавить VSM для каждого элемента управления, а затем переключать визуальные состояния на основе представления.

5. @stijn Когда вы говорите, что выставляете 3 свойства на виртуальной машине, будут ли эти свойства иметь цвет? Если это так, это как бы помещает информацию о представлении в ViewModel.

Ответ №1:

Почему бы вам просто не объявить обычный класс, который реализует INotifyPropertyChanged интерфейс для вашего Status объекта? Просто добавьте в него необходимые свойства, такие как Geometry , Fill и Stroke и т. Д. Если бы вы это сделали, вам не понадобились бы никакие Converter классы, и вы могли бы сделать что-то вроде этого:

 <DataTemplate x:Key="PanelItemTemplate">
    <Viewbox MaxHeight="20" HorizontalAlignment="Left" Cursor="Hand" > 
           <WrapPanel>
                <Path Margin="2,2,2,2" StrokeThickness="2" >
                <Path.Data>
                    <Binding Path="Status.Geometry" Mode="OneWay" />
                </Path.Data>
                <Path.Fill>
                    <Binding Path="Status.Fill" Mode="OneWay" />
                </Path.Fill>
                <Path.Stroke>
                    <Binding Path="Status.Stroke" Mode="OneWay" />
                </Path.Stroke>
            </Path>
            <ContentPresenter  Content="{Binding Path=Name}" Margin="5,0,0,0"   />
        </WrapPanel>
    </Viewbox>
</DataTemplate>
 

Комментарии:

1. Имейте в виду, что свойства, подобные Fill и Stroke представляющие визуальное состояние, не должны быть частью вашей viewmodel, если вы намерены правильно следовать шаблону MVVM.

2. «должен» не означает «должен»

3. Мне нравится идея, я могу создать класс, который охватывает эту логику. Мой вопрос в том, где он подходит? DataContext представления устанавливается в мою ViewModel, ItemsControl привязан к коллекции элементов в этой ViewModel, как мне вставить новый класс состояния между ними?

4. Я согласен с Adi Fill и Stroke не должен быть частью вашей виртуальной машины, поскольку представление должно определять визуальное состояние на основе содержимого виртуальной машины. Вы можете нарушить правило, но если вы передаете представления дизайнеру UX / UI, и им приходится возиться с viewmodels, чтобы изменить визуальное состояние, это как бы сводит на нет всю цель MVVM. Есть простые способы заставить визуальное состояние реагировать на свойства viewmodel с помощью триггеров или VSM. Вы можете быть «полустрогими» с MVVM, но если вы можете найти способ сделать это без «обмана», тогда вы учитесь и обычно создаете более надежное решение.

5. Однако — в противовес — если ваша виртуальная машина зависит от конкретного приложения и переносит определенный элемент данных, то вы можете утверждать, что он идет рука об руку с представлением. Если вы повторно используете viewmodels, то, скорее всего, это неправильный подход

Ответ №2:

Можно создать только один конвертер, который будет включать в себя всю логику преобразования и возвращать новый класс, к которому вы будете привязываться:

 public class MyNewConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Perform all required conversions here and return them as a new type of object
        return new
            {
                Data = ...,
                Fill = ...,
                Stroke = ...
            };
    }
}
 

Тогда все, что вам нужно сделать, это изменить ваше Path DataContext свойство, чтобы использовать этот конвертер.

 <Viewbox MaxHeight="20" HorizontalAlignment="Left" Cursor="Hand">
    <WrapPanel>
        <Path Margin="2,2,2,2" StrokeThickness="2" DataContext="{Binding Path=Status, Converter={StaticResource MyNewConverter}}">
            <Path.Data>
                <Binding Path="Data" Mode="OneWay" />
            </Path.Data>
            <Path.Fill>
                <Binding Path="Fill" Mode="OneWay" />
            </Path.Fill>
            <Path.Stroke>
                <Binding Path="Stroke" Mode="OneWay" />
            </Path.Stroke>
        </Path>
        <ContentPresenter Content="{Binding Path=Name}" Margin="5,0,0,0" />
    </WrapPanel>
</Viewbox>
 

Комментарии:

1. Мне очень нравится эта идея! Единственная загвоздка, с которой я только что столкнулся, — это некоторые дополнительные требования, которые требуют мигания цвета фона (может быть, раскадровки?) и звукового уведомления о определенных состояниях. Я не уверен, как запустить эти типы событий пользовательского интерфейса… Хм, разве что, возможно, раскадровку можно отключить и включить с помощью привязываемого атрибута на раскадровке? Я сейчас изучаю это.

2. Некоторое время я пользовался этим решением, пока кое-что не обнаружил. Возврат нового анонимного типа, подобного этому, возвращает несколько ошибок привязки. Это действительно работает, но ошибки трудно объяснить менеджеру сборки и влияют на производительность. В конце концов мне пришлось отказаться от этого подхода.

3. Я использовал анонимный тип для простоты. Вы могли бы создать новый класс для этого и избежать этих ошибок.