Cannot get correct WPF ComboBox behaviour (updating other controls only after FocusLost)

Viewed 39

I have a Window where users can create a question. But they should also be able to pick an old one.

This Window has a ComboBox ('QuestionComboBox') with an array of Questions. These Questions have an array of Answers and a CorrectAnswer. The QuestionComboBox is Editable, so the user can start typing and a corresponding Question will appear. But a user must also be able to create a new question. But the handling of this is not relevant here (I think). I also removed the adding of Answers here to keep the example as small as possible.

But when the user starts typing, al the controls are updating there properties (as you can see in the RightAnswerComboBox in my example when you remove the UpdateSourceTrigger=LostFocus). This appears very nervous for the user.

Requirements:

  1. The user should be able to create a new question or pick an old one from the list.
  2. And I want the RightAnswerComboBox only to be updated when the user presses Enter or on FocusLost or when he/she clicks a Question in the DropDown list to prevent a nervous user experience.

So I tried to add UpdateSourceTrigger=LostFocus to the SelectedItem. But then I get the behaviour that my ItemsSource changes as you can see in the picture. I also tried UpdateSourceTrigger=Explicit, but that I didn't get working properly at all (SelectionChanged was still called, even when the UpdateSource() was not triggered).

Q3, Q2, Q3 instead of Q1, Q2, Q3

When the user types "Q1" (and TAB), then replaces the "1" in "3" (and TAB again) then the ItemsSource is changed to Q3, Q2, Q3 instead of Q1, Q2, Q3. This is not what I want. The ItemsSource should remain the same. If I change the TwoWay mode to OneWay, the RightAnswerComboBox (and my other controls) will not be updated. So I think I need TwoWay. This unwanted behaviour also disappears when I remove the UpdateSourceTrigger=LostFocus, but then I have the nervous behaviour back.

How can I make it possible so the QuestionComboBox fulfils my requirements above?

Here is the code:

Window1.xaml

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <Window.DataContext>
        <local:Window1ViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
                <ComboBox x:Name="QuestionComboBox" Grid.Row="1" 
                      Margin="10" VerticalAlignment="Center"
                      ItemsSource="{Binding Questions, Mode=OneWay}"
                      Text="{Binding Question.Text, UpdateSourceTrigger=LostFocus}"
                      SelectedItem="{Binding DataContext.Question, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, UpdateSourceTrigger=LostFocus, Mode=TwoWay}"
                      DisplayMemberPath="Text" IsEditable="True" SelectionChanged="QuestionComboBox_SelectionChanged" />
            <ComboBox x:Name="RightAnswerComboBox" DisplayMemberPath="Text"
                          Grid.Row="2" VerticalAlignment="Center" ItemsSource="{Binding Question.Answers}" SelectedItem="{Binding Question.RightAnswer}" />
            </Grid>
    </Grid>
</Window>

Window1ViewModel.cs

class Window1ViewModel
{
    public ObservableCollection<Question> Questions { get; set; }

    public Question Question { get; set; }

    public Window1ViewModel()
    {
        var answers = new[]
        {
            new Answer(1, "A11"), new Answer(2, "A12"), new Answer(3, "A13"),
            new Answer(1, "A21"), new Answer(2, "A22"), new Answer(3, "A23"),
            new Answer(1, "A31"), new Answer(2, "A32"), new Answer(3, "A33"),
        };
        Questions = new ObservableCollection<Question>
        {
            new Question {Text="Q1", Answers = new[]
            {
                answers[0],
                answers[1],
                answers[2],
            },
                RightAnswer = answers[0]
            },
            new Question {Text="Q2", Answers = new[]
                {
                    answers[3],
                    answers[4],
                    answers[5],
                },
                RightAnswer = answers[4]
            },
            new Question {Text="Q3", Answers = new[]
                {
                    answers[6],
                    answers[7],
                    answers[8],
                },
                RightAnswer = answers[8]
            },
        };
    }
}

public class Question
{
    public Answer[] Answers { get; set; }

    public string Text { get; set; }

    public Answer RightAnswer { get; set; }
}

public class Answer : INotifyPropertyChanged
{
    private string _text;

    public Answer(int id, string text)
    {
        Id = id;
        Text = text;
    }

    public int Id { get; set; }

    public string Text
    {
        get => _text;
        set
        {
            if (value == _text)
                return;
            _text = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
1 Answers

You will get the behaviour you are looking for by not binding to the SelectedItem but to the SelectedValue. While using the item is advisable in most cases, this is one where value might be the better choise as the item that is to be selected may not yet exist.

Here is some minimal code:

<Window x:Class="CSharpSandbox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CSharpSandbox"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="400">
    <Window.DataContext>
        <local:MainVM/>
    </Window.DataContext>
    <Grid>
        <StackPanel>
            <ComboBox IsEditable="True" 
                    ItemsSource="{Binding AvaliableQuestions}"
                    SelectedValue="{Binding SelectedQuestion}"
                    Text="{Binding Selected, UpdateSourceTrigger=LostFocus}"
                    DisplayMemberPath="Name"/>
            <TextBlock Name="SomeOtherElementDisplayingAQuestionProperty"
                    Text="{Binding SelectedQuestion.Name}"/>
            <TextBox Name="MakeItLoseFocusTextBox"/>
        </StackPanel>
    </Grid>
</Window>
public class MainVM : INotifyPropertyChanged
{
    private Question _selectedQ;
    public Question SelectedQuestion
    {
        get { return _selectedQ; }
        set
        {
            if (_selectedQ != value)
            {
                _selectedQ = value;
                OnPropertyChanged();
            }
        }
    }
    private string _selectedN;
    public string SelectedName
    {
        get { return _selectedN; }
        set
        {
            if (_selectedN != value)
            {
                _selectedN = value;
                OnSelectedNameChanged(value);
                OnPropertyChanged();
            }
        }
    }
    public ObservableCollection<Question> AvaliableQuestions { get; set; }

    public MainVM ()
    {
        AvaliableQuestions = new ObservableCollection<Question>();
        AvaliableQuestions.Add(new Question("Q1"));
        AvaliableQuestions.Add(new Question("Q2"));
        AvaliableQuestions.Add(new Question("Q3"));
    }

    private void OnSelectedNameChanged(string name)
    {
        Question tempQ = AvaliableQuestions.First(q => q.Name == name);
        if (tempQ == null)
        {
            tempQ = new Question(name);
            AvaliableQuestions.Add(tempQ);
        }
        SelectedQuestion = tempQ;
    }
}


public class Question
{
    public string Name { get; private set; }

    public Question(string name)
    {
        Name = name;
    }
}
Related