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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With