2014-04-11 10 views
5

W moim modelu widoku przechowuję obiekt NewMyItem jako DataContext kontrolki odpowiedzialnej za dodanie nowego elementu na liście. Ilekroć wykonywany jest AddCommand, resetuję ten obiekt, aby był gotowy do dodania kolejnego przedmiotu.Wyzwalanie wyzwalacza ComboBox dla starego DataContext przy zmianie DataContext

Problem mam obliczu tutaj jest to, że tak szybko, jak obiekt zostanie zresetowany wewnątrz metody Add, liście rozwijanej w SelectionChanged spust jest niepotrzebnie podniesiony po właśnie dodanego elementu. Nie powinien zostać zwolniony na pierwszym miejscu, ale nawet jeśli jest on zwalniany, dlaczego zostaje zwolniony za poprzednie DataContext?

Jak tego uniknąć, ponieważ muszę wprowadzić jakąś logikę biznesową w poleceniu wyzwalacza, którego nie stać mnie na dwukrotne uruchomienie?

To prosta próba wykazania problem mam zachodu:

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-tył:

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

Jeśli umieścisz punkt przerwania wewnątrz metody ChangeType zauważysz, że niepotrzebnie uruchamia się dla dodanego elementu, gdy linia NewMyItem = new MyItem(); jest wykonywana wewnątrz metody .

Odpowiedz

2

Zamiast zdarzenie ComboBox.SelectionChanged, można użyć zdarzenia ComboBox.DropDownClosed:

występuje, gdy lista rozwijana z ComboBox zamyka.

przykład:

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

W tym przypadku ChangeType polecenie zostanie wywołana tylko raz.

+1

To rozwiązanie nie działa, gdy używasz klawiszy strzałek na klawiaturze do zmiany zaznaczenia. –

1

Ponieważ datakontekst twojego combobox jest obiektem, w poleceniu ADD ponownie zainicjujesz combobox przez nowe wystąpienie obiektu, więc wybrany element również zostanie zresetowany.

Aby uzyskać najnowszy wybrany element (wybrany przez użytkownika) lub poprzednio wybrany element (domyślny), istnieje kilka właściwości w SelectionChangedEventArgs takich jak e.AddedItems, e.RemovedItems.

niektóre przydatne dyskusje można znaleźć here za takie zastrzeżenia.

+0

Tak, wybrany element zostanie zresetowany, ale myślę, że przegapiłeś punkt, w którym 'CommandParameter' jest związany z' DataContext', więc muszę pobrać nową wartość w argumencie metody execute. – user1004959

+0

parametr jest powiązany z właściwością, która zostanie potraktowana przez powiązanie silnika między źródłem danych źródłowych a datacontext. Nie jestem pewien, dostaniesz najnowsze wartości datacontext w metodzie ChangeType() do następnego zdarzenia SelectionChanged. Czy możemy sprawdzić możliwość zapisania najnowszego obiektu z e.AddedItems do wykorzystania w przyszłości? –

1

Ma to sens - zmieniasz kontekst danych i jesteś związany z tym comboboxem SelectedValue. Zamiast używać zdarzenie Zmiana wyboru, użyję dwukierunkowa wiążące do właściwości Type:

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

Następnie uruchom logikę w seter nieruchomości (ChangeType BTW, to prawdopodobnie nie chcesz używać DependencyObject s jako klasy danych. Zamiast tego zaimplementuj INotifyPropertyChanged:

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