Silverlight 2.0 – как использовать связывание для динамического изменения цвета фона отдельного ряда DataGrid

Статья рассматривает использование связывания для динамического изменения цвета фона отдельного ряда DataGrid

Сборка System.Windows.Controls.Data содержит управляющий элемент DataGrid, служащий для отображения табличных данных в приложениях Silverlight 2.0. В то время как он успешно справляется с азами, он все же является первой версией и не поддерживает столь простые задачи, как установка фона для ряда или переключение его видимости.

DataGrid предоставляет два потенциально интересных свойства зависимости (DataGrid.RowBackgroundProperty и DataGrid.AlternatingRowBackgroundProperty), но анализ показывает, что они устанавливают фон для всех рядов. Необходима возможность устанавливать фон для каждого ряда отдельно и менять его динамически, чтобы отражать изменения в состоянии связанных сущностей.

DataGrid предоставляет событие, к которому можно прикрепиться. Событие LoadingRow передает аргумент типа DataGridRowEventArgs, предоставляющий свойство ‘ряд’, предоставляющее свойства ‘фон’ и ‘видимость’. Эти знания можно было бы использовать для установки состояния отдельных рядов, но что делать, если надо изменить состояние ряда после его загрузки. Плохо, что управляющий элемент не предоставляет коллекцию ‘ряды’, поэтому приходится хранить ссылку на ряд и обновлять ее при необходимости. Однако  это требует написания и обслуживания большого объема управляющего кода, особенно при внедрении возможностей повторного использования/виртуализации сеток в набор … данный подход неудобен.

При применении пользовательского интерфейса, не связанного с MVVM (модель-вид-модель вида), неудобно писать код, соединяющий клиентскую часть с бизнес-логикой и моделью предметной области. Надо, чтобы пользовательский интерфейс взаимодействовал с ними посредством команд (тема будущей статьи) и сохранял свое собственное состояние с помощью связываний вид/сущность. Должен быть лучший способ.

Конечно, он есть. Он сразу очевиден лишь для опытного разработчика WPF, но большинство управляющих элементов невидимы, а значит, они используют шаблоны для описания способа отрисовки своего визуального дерева. Управляющие элементы включают в себя общий шаблон, который можно заменить на любой нужный … таким путем решается данная проблема.

Во-первых, надо написать несколько объявлений пространства имен при их отсутствии в файле, который будет содержать ресурс нового стиля ряда (App.xaml)

xmlns:swc="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" 
xmlns:swcp=
"clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data"
xmlns:cvrtr='clr-namespace:iProgrammer.Demo.ValueConverters'
Создадим собственный шаблон стиля
<Style x:Name='DataGridRowStyle' TargetType="swc:DataGridRow">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="swc:DataGridRow">
<swcp:DataGridFrozenGrid Name="Root">                           
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualStateGroup.Transitions>
<vsm:VisualTransition GeneratedDuration="0" />
</vsm:VisualStateGroup.Transitions>
<vsm:VisualState x:Name="Normal" >
<Storyboard>
<DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
    Storyboard.TargetProperty="Opacity" Duration="0" To=".25"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="Normal AlternatingRow">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
    Storyboard.TargetProperty="Opacity" Duration="0" To=".25"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
    Storyboard.TargetProperty="Opacity" Duration="0" To=".75"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="Normal Selected">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
    Storyboard.TargetProperty="Opacity" Duration="0" To=".75"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOver Selected">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
    Storyboard.TargetProperty="Opacity" Duration="0" To="1"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="Unfocused Selected">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
    Storyboard.TargetProperty="Opacity" Duration="0" To=".5"/>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>

<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<Grid.Resources>
<Storyboard x:Key="DetailsVisibleTransition">
<DoubleAnimation Storyboard.TargetName="DetailsPresenter"
    Storyboard.TargetProperty="ContentHeight" Duration="00:00:0.1" />
</Storyboard>
</Grid.Resources>

<Rectangle x:Name="BackgroundRectangle"
                 Grid.RowSpan="2" Grid.ColumnSpan="2"
                 Opacity="1"
                 Fill="{Binding Path=Status,
                                     Converter={StaticResource IndexToBrushConverter},
                                     Mode=OneWay}" />


<swcp:DataGridRowHeader Grid.RowSpan="3" Name="RowHeader"
    swcp:DataGridFrozenGrid.IsFrozen="True" />
<swcp:DataGridCellsPresenter Grid.Column="1" Name="CellsPresenter"
    swcp:DataGridFrozenGrid.IsFrozen="True" />
<swcp:DataGridDetailsPresenter Grid.Row="1" Grid.Column="1" Name="DetailsPresenter" />
<Rectangle Grid.Row="2" Grid.Column="1"
    Name="BottomGridLine" HorizontalAlignment="Stretch" Height="1" />
</swcp:DataGridFrozenGrid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Как видно, визуальное дерево рядов на самом деле очень простое. Оно состоит из элемента Сетка, содержащего Прямоугольник и несколько элементов презентации, используемых DataGrid для задерживания отрисовки заголовков, информацию об отдельных ячейках и деталях ряда.

Рассмотрим первый элемент  - Прямоугольник. Прикрепленные свойства RowSpan(диапазон рядов) и ColumnSpan(диапазон стобцов) показывают, что он заполняет весь ряд (если детали не видны), и по VisualStateManager можно определить, что непрозрачность этого элемента меняется по мере того, как переключаются внутренние состояния ряда (Selected(выбран), MouseOver(наведен курсор), Focus(фокус), Alternating Row(чередующийся ряд), и т.д.). Короче говоря, когда пользователь выбирает ряд или наводит курсов мыши на него, Прямоугольник делается более прозрачным, чтобы дать пользователю визуальный отклик.

Rectangle.FillProperty (управляет фоном) является свойством зависимости в объекте зависимости, следовательно, для его динамического обновления можно использовать связывание. Надо убедиться, что связанная сущность реализует интерфейс INotifyPropertyChanged и возбуждает свое событие PropertyChanged всегда, когда меняется состояние сущности.

Надо внести минимум еще одно изменение в стиль для состояний ‘нормальное’ и ‘чередующийся ряд’. Надо немного изменить непрозрачность прямоугольника, чтобы фон был всегда виден (они должны иметь разные уровни непрозрачности).

Разумеется, вы можете добавить все что хотите в визуальное дерево или раскадровки состояний, чтобы получить желаемый реальный эффект. Для ясности в статье они сделаны простыми.

Жаль, что приходится жестко задавать путь в связывании, поскольку это требует сложного знания о свойствах, предоставляемых сущностью, связанной с контекстом данных ряда сетки. Следовательно, это не типовое решение, и все же оно лучше альтернативных решений (можно было бы изучить с помощью прикрепленных свойств, чтобы установить это, но пока применим данный подход).

Объявление связывания немного сложнее, чем надо, в том, что оно использует IValueConverter для возврата реального значения заполнения. Это надо сделать, так как неразумно хранить данные о состоянии пользовательского интерфейса в связанной сущности. Надо, чтобы она предоставляла маркер состояния и интерпретировала его с помощью помощника пользовательского интерфейса.

Ниже приведен код преобразователя:

/// <summary>
/// Преобразователь значений принимает параметр индекса и использует его для поиска в
/// массиве кистей.
/// Добавьте кисти для использования в поиске с помощью свойства Кисти данного класса
/// </summary>

public sealed class IndexToBrush : IValueConverter {

    private Brush[] _brushes = new Brush[] { new SolidColorBrush(Colors.White),
                                             new SolidColorBrush(Colors.Red),
                                             // TODO - Get appropriate RGB value
                                             // for Pink
                                             new SolidColorBrush(Colors.Orange),
                                             new SolidColorBrush(Colors.Green),
                                             new SolidColorBrush(Colors.Blue)};

    #region Property Accessors

    public Brush[] Brushes {
        get { return this._brushes; }
        set { this._brushes = value; }
    }

    #endregion

    #region IValueConverter

    public object Convert(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture) {

        if(!typeof(int).IsAssignableFrom(value.GetType()) ||
          (typeof(Brush) != targetType)) return null;

        var i = (int)value;

        if(i < 0 || i >= this._brushes.Length) return null;

        return this._brushes[i];           
    }

    public object ConvertBack(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture) {
        throw new NotImplementedException();
    }

    #endregion
}

Это действительно простая реализация. Она хранит массив экземпляров кистей, и входное связанное свойство (состояние в приведенном примере) является поиском в этом массиве.

Интересным является предоставляемое свойство (пока не поддерживаемое в Silverlight), позволяющее Xaml установить кисти при создании ресурса преобразователя.

<cvrtr:IndexToBrush x:Name='IndexToBrushConverter'>
  <cvrtr:IndexToBrush.Brushes>
      <x:Array Type='Brush'>
          <x:Static Member='Brushes.Red' />
          <x:Static Member='Brushes.Yellow' />
          <x:Static Member='Brushes.Green' />
          <x:Static Member='Brushes.Blue' />
      </x:Array>
  </cvrtr:IndexToBrush.Brushes>
</cvrtr:IndexToBrush>

Следует надеяться, что следующая версия Silverlight будет поддерживать такой синтаксис, тогда пользовательский интерфейс действительно будет отделен от базы кода.

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