2015-03-10 2 views
-2

У меня есть класс с индексом, индексированный значением enum. Мне удалось отобразить значения для этого класса в ItemsControl, установив сами значения enum как ItemsSource для управления, предоставив для элементов в элементе управления, а затем привязывая соответствующее свойство управления с помощью MultiBinding для выполнения поиск индексированного значения.Как создать привязку к косвенно указанному свойству, но который автоматически обновляется?

К сожалению, поскольку два источника Binding s из MultiBinding являются экземпляром класса и значением индекса, которые сами никогда не изменяются, отображаемое значение не обновляется при изменении индексированного значения. Связывание не знает данных, которые фактически изменяются, а только части данных, которые приводят к нему. :(

Я был в состоянии работать вокруг этого явно найти элемент шаблона для данного enum значения, извлекая его MultiBindingExpression, и называя UpdateTarget() по этому вопросу. Но это очень неудовлетворительно, не в последнюю очередь причина в том, что для что для работы, код обновления значения должен знать интимные подробности того, как данные были представлены в XAML

Вот небольшой пример, иллюстрирующий то, что я делаю & hellip;

XAML:.

<Window x:Class="TestMultiBindingUpdate.MainWindow" 
     x:ClassModifier="internal" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:local="clr-namespace:TestMultiBindingUpdate" 
     xmlns:system="clr-namespace:System;assembly=mscorlib" 
     x:Name="root" 
     Title="MainWindow" Height="350" Width="525"> 

    <Window.Resources> 
    <ObjectDataProvider x:Key="PropertyNameValues" 
         MethodName="GetValues" 
         ObjectType="{x:Type system:Enum}"> 
     <ObjectDataProvider.MethodParameters> 
     <x:Type Type="{x:Type local:PropertyName}"/> 
     </ObjectDataProvider.MethodParameters> 
    </ObjectDataProvider> 
    </Window.Resources> 

    <StackPanel> 
    <ListBox x:Name="listBox1" 
      ItemsSource="{Binding Source={StaticResource PropertyNameValues}}" 
      Margin="5"> 
     <ItemsControl.ItemTemplate> 
     <DataTemplate> 
      <StackPanel Orientation="Horizontal"> 
      <TextBlock Text="{Binding}"/> 
      <TextBlock Text=": "/> 
      <TextBlock> 
       <TextBlock.Text> 
       <MultiBinding> 
        <MultiBinding.Converter> 
        <local:PropertyNameToValueConverter/> 
        </MultiBinding.Converter> 
        <Binding ElementName="root"/> 
        <Binding/> 
       </MultiBinding> 
       </TextBlock.Text> 
      </TextBlock> 
      </StackPanel> 
     </DataTemplate> 
     </ItemsControl.ItemTemplate> 
    </ListBox> 
    <StackPanel Orientation="Horizontal"> 
     <ComboBox x:Name="comboBox1" 
       ItemsSource="{Binding Source={StaticResource PropertyNameValues}}" 
       SelectedValue="{x:Static local:PropertyName.A}" 
       Margin="5" Width="50"/> 
     <Button Content="Increment" 
       HorizontalAlignment="Center" VerticalAlignment="Center" 
       Click="Button_Click"/> 
    </StackPanel> 
    <ListBox> 
     <TextBlock Text="{Binding ElementName=root, Path=A, StringFormat=A: {0}}"/> 
     <TextBlock Text="{Binding ElementName=root, Path=B, StringFormat=B: {0}}"/> 
     <TextBlock Text="{Binding ElementName=root, Path=C, StringFormat=C: {0}}"/> 
    </ListBox> 
    </StackPanel> 
</Window> 

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

C#:

enum PropertyName 
{ 
    A, 
    B, 
    C 
} 

class PropertyNameToValueConverter : IMultiValueConverter 
{ 
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) 
    { 
     MainWindow window = (MainWindow)values[0]; 
     PropertyName propertyName = (PropertyName)values[1]; 

     return window[propertyName].ToString(); 
    } 

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

partial class MainWindow : Window 
{ 
    public static readonly DependencyProperty AProperty = 
     DependencyProperty.Register("A", typeof(int), typeof(MainWindow), new PropertyMetadata(0)); 
    public static readonly DependencyProperty BProperty = 
     DependencyProperty.Register("B", typeof(int), typeof(MainWindow), new PropertyMetadata(0)); 
    public static readonly DependencyProperty CProperty = 
     DependencyProperty.Register("C", typeof(int), typeof(MainWindow), new PropertyMetadata(0)); 

    public int A 
    { 
     get { return (int)GetValue(AProperty); } 
     set { SetValue(AProperty, value); } 
    } 

    public int B 
    { 
     get { return (int)GetValue(BProperty); } 
     set { SetValue(BProperty, value); } 
    } 

    public int C 
    { 
     get { return (int)GetValue(CProperty); } 
     set { SetValue(CProperty, value); } 
    } 

    public int this[PropertyName index] 
    { 
     get 
     { 
      switch (index) 
      { 
      case PropertyName.A: return A; 
      case PropertyName.B: return B; 
      case PropertyName.C: return C; 
      default: throw new ArgumentException(); 
      } 
     } 

     set 
     { 
      switch (index) 
      { 
      case PropertyName.A: A = value; break; 
      case PropertyName.B: B = value; break; 
      case PropertyName.C: C = value; break; 
      default: throw new ArgumentException(); 
      } 

      RefreshBinding(index); 
     } 
    } 

    private void RefreshBinding(PropertyName index) 
    { 
     ListBoxItem listBoxItem = (ListBoxItem)listBox1.ItemContainerGenerator.ContainerFromItem(index); 
     StackPanel itemStackPanel = Util.FindFirstVisualChildOfType<StackPanel>(listBoxItem); 
     TextBlock textBlock = (TextBlock)itemStackPanel.Children[2]; 

     BindingOperations.GetMultiBindingExpression(textBlock, TextBlock.TextProperty).UpdateTarget(); 
    } 

    public MainWindow() 
    { 
     InitializeComponent(); 
    } 

    private void Button_Click(object sender, RoutedEventArgs e) 
    { 
     this[(PropertyName)comboBox1.SelectedValue]++; 
    } 
} 

static class Util 
{ 
    public static T FindFirstVisualChildOfType<T>(DependencyObject o) where T : DependencyObject 
    { 
     if (o != null) 
     { 
      for (int i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++) 
      { 
       DependencyObject child = VisualTreeHelper.GetChild(o, i); 
       if (child != null) 
       { 
        T t = child as T; 

        if (t != null) 
        { 
         return t; 
        } 

        t = FindFirstVisualChildOfType<T>(child); 

        if (t != null) 
        { 
         return t; 
        } 
       } 
      } 
     } 
     return null; 
    } 
} 

Я бы очень признателен за решение этой проблемы, которая отвечает следующим критериям:

  1. Не требует ручного кодирования отдельных элементов XAML для каждого enum значения ,
  2. Не требует кода, чтобы знать, как структурируется XAML.
  3. Не требует наличия отдельного свойства в классе для каждого значения enum.

Я думаю, что толчок приходит в засуху, критерии № 3 выше договорные. Если это все Я должен был сделать, чтобы это работало лучше, это было бы не так плохо, как другие проблемы. Но я бы предпочел чисто гибкое автоматическое решение.


бонус вопрос: Поскольку мой IMultiValueConverter объект вызывается во время разработки с неправильным видом объекта, я получаю следующее исключение брошено в преобразователе на экспрессию (MainWindow)values[0]:

Невозможно для создания объекта типа «Microsoft.Expression.WpfPlatform.InstanceBuilders.WindowInstance» для ввода «TestMultiBindingUpdate.MainWindow».

Я могу обойти это, изменив конвертер, чтобы проверить тип и просто вернуться, например. Binding.DoNothing если не MainWindow type ожидает. Но тогда конвертер не работает во время разработки. Мне интересно, есть ли способ гарантировать, что во время разработки он использует тип объекта, который ожидает мой конвертер.

Я также приветствовал бы любые советы по этому поводу, если кто-то хочет предложить какие-либо.

+0

Если я правильно понял ваше решение, но метод 'RefreshBinding' зависит от пользовательского интерфейса. Значит, вы хотите сделать его независимым от пользовательского интерфейса? Или я неправильно понял? –

+0

@SriramSakthivel: да. Я не возражаю, чтобы называть дополнительный код в установщике индексатора, но я бы предпочел, чтобы он не был так тесно связан с фактическим используемым XAML. То есть Я должен иметь возможность изменять «DataTemplate» или даже связывать индексированное значение с каким-либо другим произвольным элементом пользовательского интерфейса, а не возвращаться и исправлять код, чтобы это учесть. –

ответ

1

Ваша проблема: вы связали TextBlock с номером MultiBinding с привязкой к имени элемента.

Я считаю, что невозможно определить структуру привязки, которую элемент обновил. Но, если вы привязываете TextBlock к некоторому пути привязки, вы можете сообщить об изменении данных.

Измените свой xaml, как показано ниже.

<TextBlock> 
    <TextBlock.Text> 
     <MultiBinding> 
      <MultiBinding.Converter> 
       <local:PropertyNameToValueConverter/> 
      </MultiBinding.Converter> 
      <Binding ElementName="root" Path="Item"/> 
      <Binding/> 
     </MultiBinding> 
    </TextBlock.Text> 
</TextBlock> 

Мы привязки к свойству Item (что индексатор) объекта ElementName="root".

Таким образом, вы можете уведомить механизм привязки об изменении данных через INPC.

Затем осуществить INPC в MainWindow

public partial class MainWindow : Window, INotifyPropertyChanged 

И измените индексатор поднять уведомления об изменениях.

public int this[PropertyName index] 
{ 
    get 
    { 
     switch (index) 
     { 
      case PropertyName.A: return A; 
      case PropertyName.B: return B; 
      case PropertyName.C: return C; 
      default: throw new ArgumentException(); 
     } 
    } 
    set 
    { 
     switch (index) 
     { 
      case PropertyName.A: A = value; break; 
      case PropertyName.B: B = value; break; 
      case PropertyName.C: C = value; break; 
      default: throw new ArgumentException(); 
     } 
     OnPropertyChanged("Item"); 
    } 
} 

Таким образом, интерфейс будет уведомлен об изменении данных, и вам не нужно жестко соединенный код с XAML.

Кстати, вы, кажется, не следуете за MVVM, если вы будете следовать ему, ваш код будет проверяться - что означает меньше ошибок. Я знаю, что это сокращенный пример, если вы уже следуете за MVVM в производственном коде, не обращайте внимания на это примечание.

+0

Спасибо. Должен признаться, я все еще не понимаю, почему это работает. Есть ли специальная обработка в WPF для индексатора? Или аналогичный метод будет работать для любого метода? Я был удивлен (прочитав ваш ответ), что можно привязать к чему-то, что не является простым свойством.Я даже не был уверен, что будет передано моему конвертеру; получается, что то же самое передается по-прежнему: объект, соответствующий 'ElementName =" root "', хотя обычно добавление «пути» приведет к передаче значения, которое разрешает «Путь». –

+0

Что касается предложения о MVVM, я не использую этот шаблон. На протяжении многих лет я читал несколько статей об этом, и я до сих пор не понимаю. Я придерживаюсь чего-то большего, чем простой MVC. Я не нахожу, что мой код трудно тестировать, хотя, конечно, я могу пропустить тестирование некоторых частей, которые MVVM будет облегчать. Моя большая проблема заключается в том, что MVVM, кажется, добавляет еще один слой в уже глубокий слой торта, удваивая мои усилия вперед. Если у вас есть ссылка на статью _good_, которая объясняет и оправдывает использование MVVM, я бы хотел услышать об этом. Благодаря! –

+0

(Только для того, чтобы быть понятным о MVVM/MVC: пример кода здесь действительно отличается от моего обычного шаблона, поскольку модель была объединена с моим подклассом 'Window'. Обычно я реализую модель отдельно, используя« Окно » 'подкласс и другие объекты пользовательского интерфейса, выступая в качестве контроллеров, но я с готовностью признаю, что я не пишу MVVM :)). –