Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ComboBox trigger firing for old DataContext on changing DataContext

So I keep an object NewMyItem in my view model as the DataContext of the control responsible for adding a new item in the list. Whenever the AddCommand is executed, I reset that object so it is ready for the addition of another item.

The issue I'm facing here is that as soon as the object is reset inside the Add method, the combo-box's SelectionChanged trigger is unnecessarily raised for the just added item. It shouldn't get fired in first place, but even if it is getting fired, why is it getting fired for the previous DataContext?

How to avoid this since I need to place some business logic in the trigger's command which I cannot afford to run twice?

This is a simple sample demonstrating the issue I'm facing:

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>

Code-behind:

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 );
    }
  }
}

If you put a breakpoint inside the ChangeType method, you'll notice that it unnecessarily runs for the just added item when the line NewMyItem = new MyItem(); is executed inside the Add method.

like image 654
user1004959 Avatar asked Oct 02 '22 01:10

user1004959


2 Answers

Instead of using the ComboBox.SelectionChanged event, you can use the ComboBox.DropDownClosed event:

Occurs when the drop-down list of the ComboBox closes.

Example:

<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>

In this case ChangeType command it will be called only once.

like image 119
Anatoliy Nikolaev Avatar answered Oct 13 '22 11:10

Anatoliy Nikolaev


As the datacontext of your combobox is an object, in ADD Command you are reinitialize combobox by new object instance, so it's selected item also gets reset.

In order to get latest selected item(user selected) or previously selected item(default), there are some properties in SelectionChangedEventArgs like e.AddedItems, e.RemovedItems.

some useful discussion can be found here for such caveats.

like image 27
Palak Bhansali Avatar answered Oct 13 '22 12:10

Palak Bhansali