更改DataContext时为旧DataContext触发ComboBox触发器

本文关键字:DataContext ComboBox 触发器 触发 更改 | 更新日期: 2023-09-27 17:57:47

因此,我在视图模型中保留了一个对象NewMyItem,作为负责在列表中添加新项的控件的DataContext。每当执行AddCommand时,我都会重置该对象,以便为添加另一项做好准备。

我在这里面临的问题是,一旦对象在Add方法内重置,组合框的SelectionChanged触发器就会不必要地为刚刚添加的引发。它一开始不应该被解雇,但即使它被解雇了,为什么它会因为之前的DataContext而被解雇?

如何避免这种情况,因为我需要在触发器的命令中放置一些业务逻辑,而我不能运行两次?

这是一个简单的例子,展示了我面临的问题:

XAML:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:local="clr-namespace:WpfApplication2"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <local:ChangeTypeConverter x:Key="changeTypeConverter" />
        <local:MyItems x:Key="myItems">
            <local:MyItem Name="Item 1" Type="1" />
            <local:MyItem Name="Item 2" Type="2" />
            <local:MyItem Name="Item 3" Type="3" />
        </local:MyItems>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" DataContext="{Binding DataContext.NewMyItem, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MainWindow}}}">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <TextBox Grid.Column="0" Width="100" Text="{Binding Name, Mode=TwoWay}" />
            <ComboBox Grid.Column="1" Margin="10,0,0,0" Width="40" SelectedValue="{Binding Type, Mode=OneWay}"
                      ItemsSource="{Binding DataContext.Types, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MainWindow}}}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="SelectionChanged">
                        <i:InvokeCommandAction Command="{Binding DataContext.ChangeTypeCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MainWindow}}}">
                            <i:InvokeCommandAction.CommandParameter>
                                <MultiBinding Converter="{StaticResource changeTypeConverter}">
                                    <Binding />
                                    <Binding Path="SelectedValue" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBox}}" />
                                </MultiBinding>
                            </i:InvokeCommandAction.CommandParameter>
                        </i:InvokeCommandAction>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </ComboBox>
            <Button Grid.Column="2" Margin="10,0,0,0"
                    Command="{Binding DataContext.AddCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MainWindow}}}">Add</Button>
        </Grid>
        <ListBox Grid.Row="1" ItemsSource="{StaticResource myItems}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Grid.Column="0" Width="100" Text="{Binding Name}" Foreground="Black" />
                        <TextBlock Grid.Column="1" Margin="10,0,0,0" Text="{Binding Type, StringFormat='Type {0}'}" Foreground="Black" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

代码背后:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfApplication2
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public ICommand AddCommand { get; private set; }
    public ICommand ChangeTypeCommand { get; private set; }
    public IEnumerable<int> Types { get; private set; }
    public static readonly System.Windows.DependencyProperty NewMyItemProperty = System.Windows.DependencyProperty.Register( "NewMyItem", typeof( MyItem ), typeof( MainWindow ) );
    public MyItem NewMyItem { get { return (MyItem) GetValue( NewMyItemProperty ); } protected set { SetValue( NewMyItemProperty, value ); } }
    public MainWindow()
    {
      InitializeComponent();
      Types = new List<int> { 1, 2, 3 };
      NewMyItem = new MyItem();
      AddCommand = new MyCommand( Add );
      ChangeTypeCommand = new MyCommand<Tuple<MyItem, int>>( ChangeType );
    }
    private void Add()
    {
      MyItems myItems = Resources[ "myItems" ] as MyItems;
      myItems.Add( NewMyItem );
      NewMyItem = new MyItem();
    }
    private void ChangeType( Tuple<MyItem, int> tuple )
    {
      MyItem myItem = tuple.Item1;
      int type = tuple.Item2;
      myItem.Type = type;
      // TODO : some business checks
      // if(myItem.Type == 1)
      // if(myItem.Type == 2)
      // ...
    }
  }
  public class ChangeTypeConverter : IMultiValueConverter
  {
    public object Convert( object[] values, Type targetType, object parameter, CultureInfo culture )
    {
      if( values != null && values.Length > 1 && values[ 0 ] is MyItem && values[ 1 ] is int )
        return new Tuple<MyItem, int>( (MyItem) values[ 0 ], (int) values[ 1 ] );
      return values;
    }
    public object[] ConvertBack( object value, Type[] targetTypes, object parameter, CultureInfo culture )
    {
      throw new NotSupportedException();
    }
  }
  public class MyItem : DependencyObject
  {
    public static readonly DependencyProperty NameProperty = DependencyProperty.Register( "Name", typeof( string ), typeof( MyItem ) );
    public string Name { get { return (string) GetValue( NameProperty ); } set { SetValue( NameProperty, value ); } }
    public static readonly DependencyProperty TypeProperty = DependencyProperty.Register( "Type", typeof( int ), typeof( MyItem ) );
    public int Type { get { return (int) GetValue( TypeProperty ); } set { SetValue( TypeProperty, value ); } }
  }
  public class MyItems : ObservableCollection<MyItem>
  {
  }
  public class MyCommand : ICommand
  {
    private readonly Action executeMethod = null;
    private readonly Func<bool> canExecuteMethod = null;
    public MyCommand( Action execute )
      : this( execute, null )
    {
    }
    public MyCommand( Action execute, Func<bool> canExecute )
    {
      executeMethod = execute;
      canExecuteMethod = canExecute;
    }
    public event EventHandler CanExecuteChanged;
    public void NotifyCanExecuteChanged( object sender )
    {
      if( CanExecuteChanged != null )
        CanExecuteChanged( sender, EventArgs.Empty );
    }
    public bool CanExecute( object parameter )
    {
      return canExecuteMethod != null ? canExecuteMethod() : true;
    }
    public void Execute( object parameter )
    {
      if( executeMethod != null )
        executeMethod();
    }
  }
  public class MyCommand<T> : ICommand
  {
    private readonly Action<T> executeMethod = null;
    private readonly Predicate<T> canExecuteMethod = null;
    public MyCommand( Action<T> execute )
      : this( execute, null )
    {
    }
    public MyCommand( Action<T> execute, Predicate<T> canExecute )
    {
      executeMethod = execute;
      canExecuteMethod = canExecute;
    }
    public event EventHandler CanExecuteChanged;
    public void NotifyCanExecuteChanged( object sender )
    {
      if( CanExecuteChanged != null )
        CanExecuteChanged( sender, EventArgs.Empty );
    }
    public bool CanExecute( object parameter )
    {
      return canExecuteMethod != null && parameter is T ? canExecuteMethod( (T) parameter ) : true;
    }
    public void Execute( object parameter )
    {
      if( executeMethod != null && parameter is T )
        executeMethod( (T) parameter );
    }
  }
}

如果在ChangeType方法中放置断点,您会注意到,当在Add方法中执行行NewMyItem = new MyItem();时,它不必要地为刚刚添加的项运行。

更改DataContext时为旧DataContext触发ComboBox触发器

不使用ComboBox.SelectionChanged事件,而是可以使用ComboBox.DropDownClosed事件:

ComboBox的下拉列表关闭时发生。

示例:

<ComboBox Name="MyComboBox" Grid.Column="1" Margin="10,0,0,0" Width="40" SelectedValue="{Binding Type, Mode=OneWay}"                     
            ItemsSource="{Binding DataContext.Types, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MainWindow}}}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="DropDownClosed"
                        SourceObject="{Binding ElementName=MyComboBox}">
            <i:InvokeCommandAction Command="{Binding DataContext.ChangeTypeCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MainWindow}}}">
                <i:InvokeCommandAction.CommandParameter>
                    <MultiBinding Converter="{StaticResource changeTypeConverter}">
                        <Binding />
                        <Binding Path="SelectedValue" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBox}}" />
                    </MultiBinding>
                </i:InvokeCommandAction.CommandParameter>
            </i:InvokeCommandAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ComboBox>

在这种情况下,ChangeType命令将只被调用一次。

由于组合框的数据上下文是一个对象,在ADD命令中,您将通过新的对象实例重新初始化组合框,因此它的选定项也将重置。

为了获得最近选择的项目(用户选择)或以前选择的项目,SelectionChangedEventArgs中有一些属性,如e.AddedItems、e.RemovedItems.

对于这些注意事项,可以在这里找到一些有用的讨论。

这是有意义的-您正在更改数据上下文,并且组合框的SelectedValue绑定到它。我不使用选择更改事件,而是使用到Type属性的双向绑定:

<ComboBox SelectedValue="{Binding Type}" />

然后在属性setter中运行ChangeType逻辑(顺便说一句,您可能不想使用DependencyObjects作为数据类。请实现INotifyPropertyChanged):

public int Type
{
    get { return _type; }
    set
    { 
        _type = value;
        OnPropertyChanged("Type");
        ChangeType(value);
    }
}