Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve focus-reset to update BindingSource of TextBox before any action

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; }
  }
}
like image 907
Simon D. Avatar asked Jul 15 '09 19:07

Simon D.


People also ask

How do you control when the TextBox updates the source?

If you want the source to be updated as you type, set the UpdateSourceTrigger of the binding to PropertyChanged. In the following example, the highlighted lines of code show that the Text properties of both the TextBox and the TextBlock are bound to the same source property.

How do I bind a text box in WPF?

One-Way Data Binding First of all, create a new WPF project with the name WPFDataBinding. The following XAML code creates two labels, two textboxes, and one button and initializes them with some properties.

How do I bind a class property to a TextBox in WPF?

Simply set the Binding property UpdateSourceTrigger to PropertyChanged, That will make sure whenever your TextBox Text changes the source will be updated. To force WPF to update your source property to update every time the Bound property (- in your case TextBox.

What is two way binding in WPF?

Two way binding is used when we want to update some controls property when some other related controls property change and when source property change the actual control also updates its property.


2 Answers

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.

like image 89
Simon D. Avatar answered Oct 21 '22 21:10

Simon D.


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.

like image 38
luvieere Avatar answered Oct 21 '22 23:10

luvieere