Обновление каскадных данных в контролах ComboBox в MVVM (PRISM)
WPF, MVVM, Silverlight | создано: 11.01.2011 | опубликовано: 11.01.2011 | обновлено: 13.01.2024 | просмотров: 6573
Возникла потребность каскадного обновления контролов (например, ComboBox или ListBox). То есть требуется заполнять подчиненный контрол в зависимости от выбранного значения в мастер-контроле. В интернете, как ни странно, ничего полезного не нашел, вот и решил написать эту статью.
Хотелка
Хочется выбирать значение в контролах, например ComboBox, чтобы второй контрол заполнялся значениями на основании выбора в первом, а третий - на основании выбора во втором контроле.
Итак, что мы имеем
У нас есть Silverlight-приложение, которое построено по принципу паттерна MVVM (я буду пользовать библиотеки PRISM). А раз так, то у меня есть View и ViewModel. Кстати, привязка ViewModel производится при помощи Экспорта/Импорта, то есть по средствам MEF. Так же для начала я создал некоторые папки:
рис.1
Что же в XAML
Подготовим представление (View). Нарисуем контролы. Тут всё просто без излишеств, привед код только последнего контрола, который отображает продукты:
<StackPanel Margin="20,0,20,30" VerticalAlignment="Top"> <TextBlock Height="23" HorizontalAlignment="Left" Text="Товары:" /> <ComboBox ItemsSource="{Binding Data.Products}" HorizontalAlignment="Left" VerticalAlignment="Top" ItemTemplate="{StaticResource CatalogTemplate}" Width="211" /> </StackPanel>
А вот код файла со стилями:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <DataTemplate x:Key="CatalogTemplate"> <Border Background="{Binding Color}" Padding="5"> <TextBlock FontSize="16" Text="{Binding Name}" /> </Border> </DataTemplate> <Style TargetType="ComboBoxItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> </Style> </ResourceDictionary>
Не "Топ" но всё-таки модели
В папке Models лежат файлы, которые реализуют модели проекта. Перечислю все. Первый - это каталог:
public class Catalog { public int ID { get; set; } public string Name { get; set; } public string Color { get; set; } }
далее это раздел каталога:
public class SubCatalog { public int ID { get; set; } public int ParentID { get; set; } public string Name { get; set; } public string Color { get; set; } }
и, собственно говоря, сам товар:
public class Product { public int ID { get; set; } public int ParentID { get; set; } public string Name { get; set; } public string Color { get; set; } }
Есть еще и класс, который наполняет эти классы статичными данными. Но я хочу остановиться на классе ShellViewModel, который, как мне кажется, более инересен. В этом классе есть два поля, которые являются BackStore для свойств:
public Catalog Catalog { get { return catalog; } set { catalog = value; RaisePropertyChanged(() => this.Catalog); } } public SubCatalog SubCatalog { get { return subcatalog; } set { subcatalog = value; RaisePropertyChanged(() => this.SubCatalog); } }
И снова XAML
Как не трудно догадаться, эти поля хранят выбранное значение контролов. В XAML мы устанавливаем привязку таким образом для свойства Catalog:
SelectedItem="{Binding Catalog, Mode=TwoWay}"
и также для SubCatalog:
SelectedItem="{Binding SubCatalog, Mode=TwoWay}"
Обратите внимание на режим привязки, он двунаправленный. Теперь при выборе какого-либо каталога или раздела каталога, наш ViewModel "узнает" об этом. Нам остается подписаться на событие SelectionChanged этих контролов, но как же это сделать если code-behind файл должен в MVVM оставать "пустым". Оказывает, всё уже придумано до нас. В сборке Microsoft.Expression.Interactivity.dll уже есть behavior (элемент управления поведением), который выполнит за нас всю работу. Называется он CallMethodAction. Подробное описание и способ использования можно почитать на MSDN или Blend SDK. У CallMethodAction есть свойство MethodName, вот эти методы и придется написать во ViewModel в довершение всей проделанной работы. Результат работы методов - отфильтрованный набор данных для каждого контрола.
Для первого контрола, который отображает Catalog я установил метод UpdateSubCatalog:
<i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <ei:CallMethodAction MethodName="UpdateSubCatalog" TargetObject="{Binding}" /> </i:EventTrigger> </i:Interaction.Triggers>
Ибо при изменении каталога, должен обновиться контрол, который отображает разделы. А для контрола, который отображает SubCatalog я установил метод UpdateProducts:
<i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <ei:CallMethodAction TargetObject="{Binding}" MethodName="UpdateProducts" /> </i:EventTrigger> </i:Interaction.Triggers>
В довершении хочу заметить, чтобы данные строчки XAML-разметки легко можно было вставить в код, надо добавить два namespace, которые выглядят так:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Весь код можно увидеть, скачав файл проекта (ссылка в конце статьи).
Бонус или умная мыслЯ
Пока писал статью понял, что не всё осветил в ней. Появился вопрос: "А как на счет того, если нужно не просто отфильтровать содержимое селектора (свойство ItemsSource у ListBox или ComboBox и т.д.), а изменить вид визуального представления (View) без использования code-behind?". Посмотрите на картинку справа. Вариант номер 1 мы рассмотрели. Всё работает всё прекрасно. А как на счет второго варианта? Надо чтобы при значения "пол" подставлялись другие поля для заполнения. Тут тоже можно использовать класс CallMethodAction, но есть и альтернативы.
Вед у нас всего-навсего просто несколько вариантов визуального состояния, а ими можно управлять из ViewModel тоже достаточно просто. Для этого тоже существует уже готовое управление поведением (Behavior), только называется он DataStateBehavior (MSDN). Именно его я и применил для смены состояния предварительно создав три состояния (VisualState):
<VisualStateGroup x:Name="VisualStateGroup"> <VisualState x:Name="NotSelected"/> <VisualState x:Name="Male"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="stackPanel"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Female"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="stackPanel1"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup>
А вот и пример использования DataStateBehavior:
<i:Interaction.Behaviors> <ei:DataStateBehavior Binding="{Binding Sex}" Value="Male" TrueState="Male" FalseState="Female"/> </i:Interaction.Behaviors>
Вот и всё.