Обновление каскадных данных в контролах ComboBox в MVVM (PRISM)

ru-RU | создано: 11.01.2011 | опубликовано: 11.01.2011 | обновлено: 01.01.2018 | просмотров за всё время: 5375

Возникла потребность каскадного обновления контролов (например, 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>

Вот и всё.