3

Hi am pretty new to WPF and I have started to implement an app using mvvm pattern based on the mvvm light framework. I found it great but I got a problem using two EventToCommand on controls that are supposed to interact together. I am guessing I am doing something wrong... would you help me to find out what exactly?

I have one window with two controls: a combo box which allows the selection of a Name and a TextBox that displays a Caption. A name has a default caption (hardcoded for the example in the constructor of the ViewModel, see below). So when the user select a name the textbox should display the default caption. However, the caption is editable, which means that the user can change the Caption (as long as he does not change the Name again).

In the example below, I have implemented this using MVVM pattern with MVVM Light framework. The Ok button, is only bound to a command that logs the value in the OutPut window (to see properties values in the ViewModel).

As you will see in the source code comment, the problem comes from the fact that NameSelectionChanged command triggers the CaptionTextChanged command with an "outdated value". For now, I implemented a hacky workaround (not in the code below) by setting a boolean value that ignores the code in CaptionTextChanged when executing the RaisePropertyChanged in NameSelectionChanged but it is not really satisfactory.

the View in XAML

<Window x:Class="TwoControls.MainWindow"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:vm="clr-namespace:TwoControls"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"
        mc:Ignorable="d" 
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <vm:DummyViewModelLocator x:Key="Locator" d:IsDataSource="True" />
    </Window.Resources>
    <Window.DataContext>
        <Binding Path="GetViewModel" Source="{StaticResource Locator}" />
    </Window.DataContext>
    <Grid>
        <ComboBox ItemsSource="{Binding ColumnNames}"  x:Name="NamesComboBox" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectionChanged">
                    <cmd:EventToCommand Command="{Binding NameSelectionChanged}" CommandParameter="{Binding SelectedValue, ElementName=NamesComboBox}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </ComboBox>
        <TextBox Text="{Binding Caption, Mode=TwoWay}" Name="CaptionTextBox"  HorizontalAlignment="Left" Height="23" Margin="0,45,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="TextChanged">
                    <cmd:EventToCommand Command="{Binding CaptionTextChanged}" CommandParameter="{Binding Text, ElementName=CaptionTextBox}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </TextBox>
        <Button Content="Ok" Command="{Binding ClickOk}" HorizontalAlignment="Left" Margin="120,170,0,0" VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

the view model in C#

public class MainViewModel : ViewModelBase
        {
            private readonly List<string> _names;
            private readonly string[] _captions;

            public MainViewModel()
            {
                _names = new List<string>(new[]{"TOTO","TATA","TUTU"});
                _captions = new[] {"toto", "tata", "tutu"};
            }

            public string Name { get; set; }

            public string Caption { get; set; }

            public ICommand NameSelectionChanged
            {
                get
                {
                    return new RelayCommand<string>((input) =>
                        {
                            Name = input;
                            int index = _names.IndexOf(input);
                            Caption = _captions[index];

                            //Trigger the execution of CaptionTextChanged with the old value of the TextBox
                            //even if Caption and TextBox.Text are bound TwoWay....
                            base.RaisePropertyChanged(()=>this.Caption);
                        });
                }
            }

            public ICommand CaptionTextChanged
            {
                get
                {
                    return new RelayCommand<string>((input) =>
                        {
                            Caption = input;
                        });
                }
            }


            public ICommand ClickOk
            {
                get
                {
                    return new RelayCommand(() =>
                    {
                        Console.WriteLine("Name=" + Name +";" +"Caption=" + Caption);
                    });
                }
            }

            public List<string> ColumnNames
            {
                get { return _names; }
            } 
        }

Ps: targeted .NET is 3.5 and MVVMLight's version is 4.1.27.1

Benoit Patra
  • 4,355
  • 5
  • 30
  • 53

1 Answers1

1

You don't need to use any events to do this in WPF. All this is possible by just using property Bindings if you implement the INotifyPropertyChanged interface on your view model as is customary... how about this:

private ObservableCollection<string> columnNames = new 
    ObservableCollection<string>();
public ObservableCollection<string> ColumnNames
{
    get { return columnNames; }
    set { columnNames = value; NotifyPropertyChanged("ColumnNames"); }
}

private string selectedColumnName;
public string SelectedColumnName
{
    get { return selectedColumnName; }
    set 
    { 
        selectedColumnName = value;
        NotifyPropertyChanged("SelectedColumnName");
        int index = _names.IndexOf(value); // <<< Changes are reflected here
        Caption = _captions[index];
    }
}

private string caption = string.Empty;
public string Caption 
{
    get { return caption; }
    set { caption = value; NotifyPropertyChanged("Caption"); }
}

In XAML:

<Grid>
    <ComboBox ItemsSource="{Binding ColumnNames}" SelectedItem="{Binding 
        SelectedColumnName}" x:Name="NamesComboBox" HorizontalAlignment="Left" 
        VerticalAlignment="Top" Width="120" />
    <TextBox Text="{Binding Caption, Mode=TwoWay}" Name="CaptionTextBox" 
        HorizontalAlignment="Left" Height="23" Margin="0,45,0,0" TextWrapping="Wrap" 
        VerticalAlignment="Top" Width="120" />
    <Button Content="Ok" Command="{Binding ClickOk}" HorizontalAlignment="Left" 
        Margin="120,170,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>

Let me know how it goes.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • That's perfect. Actually I was using EventToCommand to get the TextChanged event in order to enable/disable the Ok button depending on the value of caption (not represented in the example above). By default the property of the source is only updated after loosing focus, the mistake came from the fact that I was missing the UpdateSourceTrigger property http://stackoverflow.com/questions/8912456/fire-textbox-textchanged-immediately-when-text-is-typed-in Thank you – Benoit Patra Oct 21 '13 at 12:45