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

5

因此,我在我的视图模型中保持一个名为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();这行代码时,它会不必要地为刚添加的项目运行。

3个回答

2

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

ComboBox.DropDownClosed 事件在下拉列表关闭时发生。

示例:

<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 命令只会被调用一次。

1
当使用键盘箭头键更改选择时,此解决方案无法正常工作。 - Eli Arbel

1
这很有道理 - 您正在更改数据上下文,组合框的 SelectedValue 与其绑定。不要使用选择更改事件,而是使用 Type 属性的双向绑定:
<ComboBox SelectedValue="{Binding Type}" />

然后在属性设置器中运行ChangeType逻辑(顺便说一句,您可能不想将DependencyObject用作数据类。改为实现INotifyPropertyChanged):

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

1

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

为了获取最新选定的项目(用户选择)或以前选定的项目(默认值),SelectionChangedEventArgs 中有一些属性,例如 e.AddedItems、e.RemovedItems。

可在此处找到有关此类注意事项的一些有用讨论。


是的,选定的项目会被重置,但我认为你错过了CommandParameter绑定到DataContext的重点,所以我必须在执行方法的参数中获取新值。 - user1004959
参数绑定到属性,这将由绑定引擎在源-目标数据上下文之间处理。我不确定在ChangeType()方法中是否会获取到最新的数据上下文值,直到下一个SelectionChanged事件。我们能否检查从e.AddedItems保存最新对象的可能性以供将来使用? - Palak Bhansali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接