WPF Databind Before Saving
Asked Answered
R

12

41

In my WPF application, I have a number of databound TextBoxes. The UpdateSourceTrigger for these bindings is LostFocus. The object is saved using the File menu. The problem I have is that it is possible to enter a new value into a TextBox, select Save from the File menu, and never persist the new value (the one visible in the TextBox) because accessing the menu does not remove focus from the TextBox. How can I fix this? Is there some way to force all the controls in a page to databind?

@palehorse: Good point. Unfortunately, I need to use LostFocus as my UpdateSourceTrigger in order to support the type of validation I want.

@dmo: I had thought of that. It seems, however, like a really inelegant solution for a relatively simple problem. Also, it requires that there be some control on the page which is is always visible to receive the focus. My application is tabbed, however, so no such control readily presents itself.

@Nidonocu: The fact that using the menu did not move focus from the TextBox confused me as well. That is, however, the behavior I am seeing. The following simple example demonstrates my problem:

<Window x:Class="WpfApplication2.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <ObjectDataProvider x:Key="MyItemProvider" />
    </Window.Resources>
    <DockPanel LastChildFill="True">
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="File">
                <MenuItem Header="Save" Click="MenuItem_Click" />
            </MenuItem>
        </Menu>
        <StackPanel DataContext="{Binding Source={StaticResource MyItemProvider}}">
            <Label Content="Enter some text and then File > Save:" />
            <TextBox Text="{Binding ValueA}" />
            <TextBox Text="{Binding ValueB}" />
        </StackPanel>
    </DockPanel>
</Window>
using System;
using System.Text;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication2
{
    public partial class Window1 : Window
    {
        public MyItem Item
        {
            get { return (FindResource("MyItemProvider") as ObjectDataProvider).ObjectInstance as MyItem; }
            set { (FindResource("MyItemProvider") as ObjectDataProvider).ObjectInstance = value; }
        }

        public Window1()
        {
            InitializeComponent();
            Item = new MyItem();
        }

        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(string.Format("At the time of saving, the values in the TextBoxes are:\n'{0}'\nand\n'{1}'", Item.ValueA, Item.ValueB));
        }
    }

    public class MyItem
    {
        public string ValueA { get; set; }
        public string ValueB { get; set; }
    }
}
Rorqual answered 11/9, 2008 at 20:8 Comment(0)
E
6

Suppose you have a TextBox in a window, and a ToolBar with a Save button in it. Assume the TextBox’s Text property is bound to a property on a business object, and the binding’s UpdateSourceTrigger property is set to the default value of LostFocus, meaning that the bound value is pushed back to the business object property when the TextBox loses input focus. Also, assume that the ToolBar’s Save button has its Command property set to ApplicationCommands.Save command.

In that situation, if you edit the TextBox and click the Save button with the mouse, there is a problem. When clicking on a Button in a ToolBar, the TextBox does not lose focus. Since the TextBox’s LostFocus event does not fire, the Text property binding does not update the source property of the business object.

Obviously you should not validate and save an object if the most recently edited value in the UI has not yet been pushed into the object. This is the exact problem Karl had worked around, by writing code in his window that manually looked for a TextBox with focus and updated the source of the data binding. His solution worked fine, but it got me thinking about a generic solution that would also be useful outside of this particular scenario. Enter CommandGroup…

Taken from Josh Smith’s CodeProject article about CommandGroup

Everything answered 12/9, 2008 at 7:22 Comment(1)
This solution only works for TextBox. Is there a way to make it work for any control?Chalet
T
29

I found that removing the menu items that are scope depended from the FocusScope of the menu causes the textbox to lose focus correctly. I wouldn't think this applies to ALL items in Menu, but certainly for a save or validate action.

<Menu FocusManager.IsFocusScope="False" >
Trug answered 19/11, 2009 at 16:59 Comment(7)
This does solve the original problem (same one I was having). My setter now gets called when menu buttons are pressed and I can leave LostFocus as the UpdateSourceTrigger.Equipoise
To me it looks like the best solution presented. Thanks.Bove
oh wow... i answered this a long time ago~ glad i could help :)Trug
+1 I'm currently going through similar questions to make sure this answer is present because it's the best!Knotts
This will leave focus on toolbar button.Isar
It seems one of the design principle is broken if doing it in this way. The behavior of one control itself depends on whether other controls are willing to accept the focus.Waterhouse
OMG. thank you from 14 years in the future.Presumably
P
16

Assuming that there is more than one control in the tab sequence, the following solution appears to be complete and general (just cut-and-paste)...

Control currentControl = System.Windows.Input.Keyboard.FocusedElement as Control;

if (currentControl != null)
{
    // Force focus away from the current control to update its binding source.
    currentControl.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
    currentControl.Focus();
}
Postscript answered 18/1, 2011 at 13:44 Comment(2)
In which method should this piece of code be added? This way seems to be a forcible way no matter the other control would like to accept the focus or not, this code forces the current selected control to take the focus.Waterhouse
I used this code snip into my Save button execution method. Then other text boxes got lostFouces and trigger update source bound with that propertyHowever
E
8

This is a UGLY hack but should also work

TextBox focusedTextBox = Keyboard.FocusedElement as TextBox;
if (focusedTextBox != null)
{
    focusedTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}

This code checks if a TextBox has focus... If 1 is found... update the binding source!

Everything answered 12/9, 2008 at 7:32 Comment(1)
+1, that's the workaround I'm using right now. To make it a bit less ugly, I recommend checking the binding expression for null before calling UpdateSource (since an unbound TextBox might have the focus currently).Torruella
E
6

Suppose you have a TextBox in a window, and a ToolBar with a Save button in it. Assume the TextBox’s Text property is bound to a property on a business object, and the binding’s UpdateSourceTrigger property is set to the default value of LostFocus, meaning that the bound value is pushed back to the business object property when the TextBox loses input focus. Also, assume that the ToolBar’s Save button has its Command property set to ApplicationCommands.Save command.

In that situation, if you edit the TextBox and click the Save button with the mouse, there is a problem. When clicking on a Button in a ToolBar, the TextBox does not lose focus. Since the TextBox’s LostFocus event does not fire, the Text property binding does not update the source property of the business object.

Obviously you should not validate and save an object if the most recently edited value in the UI has not yet been pushed into the object. This is the exact problem Karl had worked around, by writing code in his window that manually looked for a TextBox with focus and updated the source of the data binding. His solution worked fine, but it got me thinking about a generic solution that would also be useful outside of this particular scenario. Enter CommandGroup…

Taken from Josh Smith’s CodeProject article about CommandGroup

Everything answered 12/9, 2008 at 7:22 Comment(1)
This solution only works for TextBox. Is there a way to make it work for any control?Chalet
M
3

Simple solution is update the Xaml code as shown below

    <StackPanel DataContext="{Binding Source={StaticResource MyItemProvider}}"> 
        <Label Content="Enter some text and then File > Save:" /> 
        <TextBox Text="{Binding ValueA, UpdateSourceTrigger=PropertyChanged}" /> 
        <TextBox Text="{Binding ValueB, UpdateSourceTrigger=PropertyChanged}" /> 
    </StackPanel> 
Miracidium answered 22/9, 2011 at 17:24 Comment(0)
M
3

I've run into this issue and the best solution I've found was to change the focusable value of the button (or any other component such as MenuItem) to true:

<Button Focusable="True" Command="{Binding CustomSaveCommand}"/>

The reason it works, is because it forces the button to get focused before it invokes the command and therefore makes the TextBox or any other UIElement for that matter to loose their focus and raise lost focus event which invokes the binding to be changed.

In case you are using bounded command (as I was pointing to in my example), John Smith's great solution won't fit very well since you can't bind StaticExtension into bounded property (nor DP).

Macrobiotic answered 23/3, 2012 at 13:33 Comment(0)
K
2

Have you tried setting the UpdateSourceTrigger to PropertyChanged? Alternatively, you could call the UpdateSOurce() method, but that seems like a bit overkill and defeats the purpose of TwoWay databinding.

Kenlay answered 11/9, 2008 at 20:14 Comment(0)
S
1

Could you set the focus somewhere else just before saving?

You can do this by calling focus() on a UI element.

You could focus on whatever element invokes the "save". If your trigger is LostFocus then you have to move the focus somewhere. Save has the advantage that it isn't modified and would make sense to the user.

Shannonshanny answered 11/9, 2008 at 21:9 Comment(0)
W
1

Since I noticed this issue is still a pain in the ass to solve on a very generic way, I tried various solutions.

Eventually one that worked out for me: Whenever the need is there that UI changes must be validated and updated to its sources (Check for changes upon closeing a window, performing Save operations, ...), I call a validation function which does various things: - make sure a focused element (like textbox, combobox, ...) loses its focus which will trigger default updatesource behavior - validate any controls within the tree of the DependencyObject which is given to the validation function - set focus back to the original focused element

The function itself returns true if everything is in order (validation is succesful) -> your original action (closeing with optional asking confirmation, saveing, ...) can continue. Otherwise the function will return false and your action cannot continue because there are validation errors on one or more elements (with the help of a generic ErrorTemplate on the elements).

The code (validation functionality is based on the article Detecting WPF Validation Errors):

public static class Validator
{
    private static Dictionary<String, List<DependencyProperty>> gdicCachedDependencyProperties = new Dictionary<String, List<DependencyProperty>>();

    public static Boolean IsValid(DependencyObject Parent)
    {
        // Move focus and reset it to update bindings which or otherwise not processed until losefocus
        IInputElement lfocusedElement = Keyboard.FocusedElement;
        if (lfocusedElement != null && lfocusedElement is UIElement)
        {
            // Move to previous AND to next InputElement (if your next InputElement is a menu, focus will not be lost -> therefor move in both directions)
            (lfocusedElement as UIElement).MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
            (lfocusedElement as UIElement).MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            Keyboard.ClearFocus();
        }

        if (Parent as UIElement == null || (Parent as UIElement).Visibility != Visibility.Visible)
            return true;

        // Validate all the bindings on the parent 
        Boolean lblnIsValid = true;
        foreach (DependencyProperty aDependencyProperty in GetAllDependencyProperties(Parent))
        {
            if (BindingOperations.IsDataBound(Parent, aDependencyProperty))
            {
                // Get the binding expression base. This way all kinds of bindings (MultiBinding, PropertyBinding, ...) can be updated
                BindingExpressionBase lbindingExpressionBase = BindingOperations.GetBindingExpressionBase(Parent, aDependencyProperty);
                if (lbindingExpressionBase != null)
                {
                    lbindingExpressionBase.ValidateWithoutUpdate();
                    if (lbindingExpressionBase.HasError)
                        lblnIsValid = false;
                }
            }
        }

        if (Parent is Visual || Parent is Visual3D)
        {
            // Fetch the visual children (in case of templated content, the LogicalTreeHelper will return no childs)
            Int32 lintVisualChildCount = VisualTreeHelper.GetChildrenCount(Parent);
            for (Int32 lintVisualChildIndex = 0; lintVisualChildIndex < lintVisualChildCount; lintVisualChildIndex++)
                if (!IsValid(VisualTreeHelper.GetChild(Parent, lintVisualChildIndex)))
                    lblnIsValid = false;
        }

        if (lfocusedElement != null)
            lfocusedElement.Focus();

        return lblnIsValid;
    }

    public static List<DependencyProperty> GetAllDependencyProperties(DependencyObject DependencyObject)
    {
        Type ltype = DependencyObject.GetType();
        if (gdicCachedDependencyProperties.ContainsKey(ltype.FullName))
            return gdicCachedDependencyProperties[ltype.FullName];

        List<DependencyProperty> llstDependencyProperties = new List<DependencyProperty>();
        List<FieldInfo> llstFieldInfos = ltype.GetFields(BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static).Where(Field => Field.FieldType == typeof(DependencyProperty)).ToList();
        foreach (FieldInfo aFieldInfo in llstFieldInfos)
            llstDependencyProperties.Add(aFieldInfo.GetValue(null) as DependencyProperty);
        gdicCachedDependencyProperties.Add(ltype.FullName, llstDependencyProperties);

        return llstDependencyProperties;
    }
}
Wundt answered 30/12, 2011 at 14:12 Comment(0)
J
0

The easiest way is to set the focus somewhere.
You can set the focus back immediately, but setting the focus anywhere will trigger the LostFocus-Event on any type of control and make it update its stuff:

IInputElement x = System.Windows.Input.Keyboard.FocusedElement;
DummyField.Focus();
x.Focus();

Another way would be to get the focused element, get the binding element from the focused element, and trigger the update manually. An example for TextBox and ComboBox (you would need to add any control type you need to support):

TextBox t = Keyboard.FocusedElement as TextBox;
if ((t != null) && (t.GetBindingExpression(TextBox.TextProperty) != null))
  t.GetBindingExpression(TextBox.TextProperty).UpdateSource();

ComboBox c = Keyboard.FocusedElement as ComboBox;
if ((c != null) && (c.GetBindingExpression(ComboBox.TextProperty) != null))
  c.GetBindingExpression(ComboBox.TextProperty).UpdateSource();
Jumbala answered 23/10, 2008 at 13:35 Comment(0)
F
0

What do you think about this? I believe I've figured out a way to make it a bit more generic using reflection. I really didn't like the idea of maintaining a list like some of the other examples.

var currentControl = System.Windows.Input.Keyboard.FocusedElement;
if (currentControl != null)
{
    Type type = currentControl.GetType();
    if (type.GetMethod("MoveFocus") != null && type.GetMethod("Focus") != null)
    {
        try
        {
            type.GetMethod("MoveFocus").Invoke(currentControl, new object[] { new TraversalRequest(FocusNavigationDirection.Next) });
            type.GetMethod("Focus").Invoke(currentControl, null);
        }
        catch (Exception ex)
        {
            throw new Exception("Unable to handle unknown type: " + type.Name, ex);
        }
    }
}

See any problems with that?

Freddafreddi answered 1/6, 2011 at 0:47 Comment(0)
O
0

Manually simulate LostFocus will help.

public void Refresh(FrameworkElement window)
{
    var editor = FocusManager.GetFocusedElement(window);
    FocusManager.SetFocusedElement(window, window);
    FocusManager.SetFocusedElement(window, editor);
}
Oech answered 13/11, 2014 at 6:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.