views:

1390

answers:

3

I have observed some unexpected or at least not-perfectly-matching-my-needs behaviour of textboxes bound to textproperties when I can't use using UpdateTrigger=PropertyChanged for my binding. Probably it is not an issue with the textbox but will occur with other editors as well.

In my example (source code attached), I have a WPF TabControl bound to some collection. On each tab, you can edit an item from the collection, in various ways you can trigger a save-action, which should save the edits to some model. The textboxes bound to each items' properties are (on purpose) kept to default update-trigger 'OnFocusLost'. This is because there is some expensive validation taking place when a new value is set.

Now I found there are at least two ways to trigger my save-action in such a way, that the last focused textbox does not update the bound value. 1) Changing the tab-item via mouse-click on its header and then clicking some save-button. (changing back to the previous tab shows that the new value is even lost) 2) Triggering the save-command via KeyGesture.

I setup an example application that demonstrates the behaviour. Clicking on "Save All" will show all item values, the other save-button only shows the current item.

Q: What would be the best way to make sure that all bindingsources of all my textboxes will be updated before the bound objects are comitted? Preferably there should be a single way that catches all possibilites, I dislike to catch each event differently, since I would worry to have forgotten some events. Observing the selection-changed-event of the tab-control for example would solve issue 1) but not issue 2).

Now to the example:

XAML first:

<Window x:Class="TestOMat.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:TestOMat="clr-namespace:TestOMat"
Title="TestOMat" x:Name="wnd">
<Grid>
    <Grid.Resources>
        <DataTemplate x:Key="dtPerson" DataType="{x:Type TestOMat:Person}">
            <StackPanel Orientation="Vertical">
                <StackPanel.CommandBindings>
                    <CommandBinding Command="Close" Executed="CmdSaveExecuted"/>
                </StackPanel.CommandBindings>
                <TextBox Text="{Binding FirstName}"/>
                <TextBox Text="{Binding LastName}"/>
                <Button Command="ApplicationCommands.Stop" CommandParameter="{Binding}">Save</Button>
            </StackPanel>
        </DataTemplate>
    </Grid.Resources>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Stop" Executed="CmdSaveAllExecuted"/>
    </Grid.CommandBindings>
    <TabControl ItemsSource="{Binding ElementName=wnd, Path=Persons}" ContentTemplate="{StaticResource dtPerson}" SelectionChanged="TabControl_SelectionChanged"/>
    <Button Grid.Row="1" Command="ApplicationCommands.Stop">Save All</Button>
</Grid></Window>

And the corresponding class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace TestOMat
{
  /// <summary>
  /// Interaction logic for TestOMat.xaml
  /// </summary>
  public partial class TestWindow : Window
  {
    public TestWindow()
    {
      InitializeComponent();
    }

private List<Person> persons = new List<Person>
              {
                new Person {FirstName = "John", LastName = "Smith"},
                new Person {FirstName = "Peter", LastName = "Miller"}
              };

public List<Person> Persons
{
  get { return persons; }
  set { persons = value; }
}

private void CmdSaveExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
  Person p = e.Parameter as Person;
  if (p != null)
  {
    MessageBox.Show(string.Format("FirstName={0}, LastName={1}", p.FirstName, p.LastName));
    e.Handled = true;
  }
}

private void CmdSaveAllExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
  MessageBox.Show(String.Join(Environment.NewLine, Persons.Select(p=>string.Format("FirstName={0}, LastName={1}", p.FirstName, p.LastName)).ToArray()));
  e.Handled = true;
}

private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  Console.WriteLine(String.Format("Selection changed from {0} to {1}", e.RemovedItems, e.AddedItems));
  // Doing anything here only avoids loss on selected-tab-change
}
  }
  public class Person
  {
    public string FirstName { get; set; }
    public string LastName { get; set; }
  }
}
A: 

Maybe set the UpdateSourceTrigger property of the binding:

<TextBox Text="{Binding FirstName, UpdateSourceTrigger=Explicit}"/>

I am not sure this is what you're looking for.

Shimmy
I'm sorry, it is not. It may work out, but if I use explicit updating, I will need to update all my bindings manually in code-behind, e.g. before saving. So if there is more than one action possible, each action has to take care of first updating all bindings, which is what I would like to avoid. (Reason for this: I want to decouple the bound object from the view according to the M-V-VM-pattern, so the Save-Command should not "know" the view at all and not access the bindings to its properties)Anyway, thanks for your answer, I had already given up receiving any feedback.
Simpzon
+1  A: 

You could write a style targeting all textboxes, in which you would have an EventSetter on the GotFocus or GotKeyboardFocus events, and on complementary LostFocus events. In the handler associated with the GotFocus events, you would set a "canSave" boolean variable to false, that in the LostFocus handler you'll set back to true. All you have to do then is to check before saving if your variable allows you too. If not, you can notify the user, or simply switch the focus from the textbox to something else. That way, the binding's update trigger for the currently edited textbox will trigger appropriately when its focus is lost.

luvieere
Unfortunately, when switching the selected tab, the TextBox in the old tab will never get the LostFocus-Event, which is a bug in WPF I would say. That is why the UpdateTrigger (being LostFocus) is never triggered automatically. So your solution will not help me in this case.Still +1 for leading me towards examining all focus-related events, I figured out a quick'n'dirty way to do it.
Simpzon
+2  A: 

Maybe it's not nice to answer own questions, but I think this answer is more suitable to the question than others, and therefore worth to be written. Surely this was also because I did not describe the problem clearly enough.

Finally, just as a quick'n'dirty proof of concept, I worked around it like this: The LostFocus-Event is never fired on the TextBox, when I switch the tab. Therefore, the binding doesn't update and the entered value is lost, because switching back makes the binding refresh from its source. But what IS fired is the PreviewLostFocus-Event, hence I hooked in this tiny function, that manually triggers the update to the binding source:

private void BeforeFocusLost(object sender, KeyboardFocusChangedEventArgs e)
{
  if (sender is TextBox) {
    var tb = (TextBox)sender;

    var bnd = BindingOperations.GetBindingExpression(tb, TextBox.TextProperty);

    if (bnd != null) {
      Console.WriteLine(String.Format("Preview Lost Focus: TextBox value {0} / Data value {1} NewFocus will be {2}", tb.Text, bnd.DataItem, e.NewFocus));
      bnd.UpdateSource();
    }
    Console.WriteLine(String.Format("Preview Lost Focus Update forced: TextBox value {0} / Data value {1} NewFocus will be {2}", tb.Text, bnd.DataItem, e.NewFocus));
  }
}

The output according to the event chain with PreviewLostFocus, LostFocus (both from TextBox) and SelectionChanged (from TabControl) will look like this:

Preview Lost Focus: TextBox value Smith123456 / Data value John Smith123 NewFocus will be System.Windows.Controls.TabItem Header:Peter Miller Content:Peter Miller Preview Lost Focus Update forced: TextBox value Smith123456 / Data value John Smith123456 NewFocus will be System.Windows.Controls.TabItem Header:Peter Miller Content:Peter Miller Selection changed from System.Object[] to System.Object[] Preview Lost Focus: TextBox value Miller / Data value Peter Miller NewFocus will be System.Windows.Controls.TextBox: Peter Preview Lost Focus Update forced: TextBox value Miller / Data value Peter Miller NewFocus will be System.Windows.Controls.TextBox: Peter Lost Focus having value Miller

We see that the LostFocus only occurs at the end, but not before changing the TabItem. Still I think this is strange, possibly a bug in WPF or in the standard control templates. Thank you all for your suggestions, sorry I couldn't really sign them to be answers, as they did not solve the loss of entries on tab-change.

Simpzon
It is certainly OK to answer your own question.
Martin Liversage