Как работать с вычисленными значениями с помощью свойств зависимости в пользовательском элементе управления WPF - PullRequest
1 голос
/ 13 апреля 2010

Чтобы подвести итог, что я делаю, у меня есть пользовательский элемент управления, который выглядит как проверенный список, и который имеет два свойства зависимости, одно из которых предоставляет список доступных параметров, а другое представляет значение флага enum, объединяющее параметры выбора. ,

Итак, как я уже упоминал, мой пользовательский элемент управления предоставляет два различных свойства DependencyProperties, один из которых представляет собой список параметров с именем Опции , а другое свойство с именем SelectedOptions имеет определенный тип Enum, который использует атрибут [Flags], чтобы разрешить установку комбинаций значений. Мой UserControl тогда содержит ItemsControl, похожий на ListBox, который используется для отображения параметров вместе с флажком. Если флажок установлен или снят, свойство SelectedOptions должно быть соответственно обновлено с использованием соответствующей побитовой операции.

Проблема, с которой я сталкиваюсь, заключается в том, что у меня нет другого выхода, кроме как поддерживать личные поля и обрабатывать события изменения свойств, чтобы обновить мои свойства, которые в WPF кажутся неестественными. Я пытался использовать ValueConverters, но столкнулся с проблемой, заключающейся в том, что я не могу использовать привязку с привязкой преобразователя значений, поэтому мне нужно прибегнуть к жесткому кодированию значений enum в качестве параметра ValueConverter, что недопустимо. Если кто-нибудь видел хороший пример того, как сделать это разумно, я был бы очень признателен за любой вклад.

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

Ответы [ 3 ]

1 голос
/ 15 апреля 2010

Другое решение

Я использую совсем другое решение для этой ситуации, которое мне кажется намного чище. Используя несколько созданных мной служебных классов, я могу напрямую связать SelectedOptions без необходимости писать код приложения для работы с командами, обновлениями коллекций и т. Д.

EnumExpansion class

Я создал простой класс со следующей подписью:

public class EnumExpansion : DependencyObject, IList, INotifyCollectionChanged
{
  public object EnumValue   { ... // DependencyProperty
  ... // IList & INotifyCollectionChanged implementation
}

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

Свойство BindableSelectedItems

Я также создал простое вложенное свойство, которое позволяет ListBox связывать его свойство SelectedItems. Используется так:

<ListBox ItemsSource="{Binding Options}"
  edf:ListBoxHelper.BindableSelectedItems="{Binding SelectedOptionsExpansion}" />

Присоединенное свойство реализуется путем подписки на SelectionChanged в ListBox и CollectionChanged на значение свойства (которое имеет тип INotifyCollectionChanged).

Инициализация SelectedOptionsExpansion

Вы можете сделать это в XAML, но это довольно просто в коде:

public EnumExpansion SelectedOptionsExpansion { get; set; }

  ...
  SelectedOptionsExpansion = new EnumExpansion();
  BindingOperations.SetBinding(SelectedOptionsExpansion, EnumExpansion.EnumValueProperty,
    new Binding { Path = "SelectedOptions", Source = this });
  ...

Как это работает

Перечисление в ListBox:

  1. Изменения в SelectedOptions через код или привязку данных
  2. Свойство EnumValue в SelectedOptionsExpansion обновляется привязкой, что приводит к изменению коллекции EnumExpansion.
  3. Событие CollectionChange регистрируется кодом ListBoxHelper, который обновляет выбор в ListBox.

ListBox для перечисления:

  1. Элемент выбран или отмените выбор в списке
  2. ListBoxHelper забирает его и обновляет коллекцию EnumExpansion, что приводит к обновлению свойства EnumValue.
  3. Поскольку EnumValue - это BindsTwoWayByDefault, значение SelectedOptions обновляется.

Почему я предпочитаю это решение

Как только два служебных класса созданы, остальная часть процесса привязки является простой привязкой данных. Нет необходимости обрабатывать команды или обновлять коллекции в коде приложения - все это скрыто в служебных классах.

1 голос
/ 14 апреля 2010

Я не могу быть уверен точно в том, что вы пытаетесь сделать, не глядя на ваш код подробно, но я думаю, что у меня есть смутное представление о вашем сценарии. Я построил для вас пример, иллюстрирующий нечто похожее на это. Вместо того, чтобы создать новый элемент управления, я поместил весь код в один Window, для простоты демонстрации. Для начала давайте посмотрим на XAML для окна:

<Window x:Class="TestWpfApplication.DataBoundFlags"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestWpfApplication"
Title="DataBoundFlags" Height="300" Width="300"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <ListBox ItemsSource="{Binding AvailableOptions}" Grid.Row="0">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding}" CommandParameter="{Binding}"
                          Command="{Binding RelativeSource={RelativeSource FindAncestor, 
                          AncestorType={x:Type Window}}, Path=SelectCommand}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <TextBlock Text="{Binding SelectedOptions}" Grid.Row="1"/>
</Grid>

В окне DataContext установлен собственный код, поэтому я могу связываться со свойствами там. У меня есть несколько свойств - AvailableOptions это все варианты, которые вы можете выбрать. SelectedOptions - это параметры, которые пользователь выбрал в данный момент. SelectCommand - это RelayCommand, который используется либо для добавления флага в SelectedOptions, либо для его удаления.

Остальная часть XAML должна быть очень простой. ListBox привязан ко всем доступным параметрам, и каждый параметр представлен как один CheckBox. Обратите особое внимание на CommandParameter, который привязан к самому пункту опции. Теперь давайте посмотрим на код, где происходит волшебство:

[Flags()]
public enum Options
{
    Plain = 0,
    Ketchup = 1,
    Mustard = 2,
    Mayo = 4,
    HotSauce = 8
}

public partial class DataBoundFlags : Window
{
    public static readonly DependencyProperty SelectedOptionsProperty =
        DependencyProperty.Register("SelectedOptions", typeof(Options), typeof(DataBoundFlags));

    public Options SelectedOptions
    {
        get { return (Options)GetValue(SelectedOptionsProperty); }
        set { SetValue(SelectedOptionsProperty, value); }
    }

    public List<Options> AvailableOptions
    {
        get
        {
            return new List<Options>()
            {
                Options.Ketchup,
                Options.Mustard,
                Options.Mayo,
                Options.HotSauce
            };
        }
    }

    public ICommand SelectCommand
    {
        get;
        private set;
    }

    /// <summary>
    /// If the option is selected, unselect it.
    /// Otherwise, select it.
    /// </summary>
    private void OnSelect(Options option)
    {
        if ((SelectedOptions & option) == option)
            SelectedOptions = SelectedOptions & ~option;
        else
            SelectedOptions |= option;
    }

    public DataBoundFlags()
    {
        SelectCommand = new RelayCommand((o) => OnSelect((Options)o));
        InitializeComponent();
    }
}

Начиная сверху, у нас есть объявление enum, за которым следует свойство зависимостей SelectedOptions и свойство AvailableOptions (которое может быть стандартным свойством CLR, поскольку оно никогда не изменится). Затем у нас есть наша команда и обработчик, который будет выполняться для команды (всякий раз, когда опция отмечена или не отмечена). Сначала обратите внимание, как команда подключена - мы создаем новый RelayCommand и сообщаем ему, чтобы он запускал OnSelect, передавая параметр команды. Помните, что это тот же параметр команды, который был связан в XAML - это означает, что текущая опция отмечена или не отмечена. Мы сравниваем эту опцию с SelectedOptions, используя побитовые операторы. Если опция существует, это означает, что мы ее снимаем, и нам нужно очистить ее, используя побитовое AND. Если он не существует, мы добавляем его в выбранное, используя побитовое ИЛИ.

Когда это происходит, свойство зависимостей SelectedOptions автоматически обновляется, что обновляет привязку TextBlock в XAML. Вот окончательный результат:

альтернативный текст http://img706.imageshack.us/img706/7269/databoundflags.png

0 голосов
/ 14 апреля 2010

Чтобы поддержать понятие значений по умолчанию, вам нужно установить привязку к свойству CheckBox.IsChecked. Вам нужен как текущий параметр (который находится на DataContext соответствующего флажка), так и свойство SelectedOptions, которое находится в окне. Таким образом, эта привязка становится:

<CheckBox.IsChecked>
    <MultiBinding Converter="{StaticResource FlagsToBoolConverter}">
        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}" 
                 Path="SelectedOptions"/>
        <Binding RelativeSource="{RelativeSource Self}" Path="DataContext"/>
    </MultiBinding>
</CheckBox.IsChecked>

FlagsToBoolConverter просто принимает их и проверяет, находится ли текущий параметр на SelectedOptions:

public class FlagsToBoolConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        Options selected = (Options)values[0];
        Options current = (Options)values[1];

        return ((selected & current) == current);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}

Теперь попробуйте установить для SelectedOptions значение по умолчанию в конструкторе. Обратите внимание, что соответствующий CheckBox автоматически проверяется, и все привязки все еще работают. Победа!

...