Обновление каскадных данных в контролах ComboBox в MVVM (PRISM)
WPF, MVVM, Silverlight | создано: 11.01.2011 | опубликовано: 11.01.2011 | обновлено: 13.01.2024 | просмотров: 6938
Возникла потребность каскадного обновления контролов (например, 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>
Вот и всё.