关于数据绑定:保存之前WPF数据绑定

关于数据绑定:保存之前WPF数据绑定

WPF Databind Before Saving

在WPF应用程序中,我有许多数据绑定的TextBox。这些绑定的UpdateSourceTriggerLostFocus。使用"文件"菜单保存该对象。我的问题是可以在TextBox中输入新值,从File菜单中选择Save,并且永远不会保留新值(TextBox中可见的值),因为访问菜单不会从TextBox中删除焦点。我怎样才能解决这个问题?有什么方法可以强制页面中的所有控件进行数据绑定吗?

@palehorse:好点。不幸的是,我需要使用LostFocus作为UpdateSourceTrigger来支持我想要的验证类型。

@dmo:我已经想到了。但是,对于一个相对简单的问题来说,这似乎是一个非常微不足道的解决方案。另外,它要求页面上有一些控件,该控件始终可见,以接收焦点。我的应用程序是选项卡式的,因此,没有这样的控件很容易出现。

@Nidonocu:使用菜单不会将焦点从TextBox移开的事实也使我感到困惑。但是,这就是我所看到的行为。下面的简单示例演示了我的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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:\
'{0}'\
and\
'{1}'", Item.ValueA, Item.ValueB));
        }
    }

    public class MyItem
    {
        public string ValueA { get; set; }
        public string ValueB { get; set; }
    }
}

我发现从菜单的FocusScope中删除范围相关的菜单项会导致文本框正确失去焦点。我不认为这适用于Menu中的所有项目,但肯定可以保存或验证操作。

1
<Menu FocusManager.IsFocusScope="False">

假设选项卡序列中有多个控件,则以下解决方案似乎是完整且通用的(只是剪切并粘贴)...

1
2
3
4
5
6
7
8
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();
}

这是一个丑陋的hack,但也应该可以

1
2
3
4
5
TextBox focusedTextBox = Keyboard.FocusedElement as TextBox;
if (focusedTextBox != null)
{
    focusedTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}

此代码检查TextBox是否具有焦点...如果找到1,则更新绑定源!


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…

摘自Josh Smith在CodeProject上有关CommandGroup的文章


一个简单的解决方案是更新Xaml代码,如下所示

1
2
3
4
5
    <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>

我遇到了这个问题,发现的最佳解决方案是将按钮(或其他任何组件,例如MenuItem)的可聚焦值更改为true:

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

之所以起作用,是因为它迫使按钮在调用命令之前就获得了焦点,并因此使TextBox或任何其他与此有关的UIElement失去了焦点并引发了丢失焦点的事件,该事件调用了要更改的绑定。

如果您使用的是有界命令(如我在示例中所指出的),则约翰·史密斯的出色解决方案将无法很好地适用,因为您无法将StaticExtension绑定到有界属性(也不是DP)中。


您是否尝试过将UpdateSourceTrigger设置为PropertyChanged?另外,您可以调用UpdateSOurce()方法,但这似乎有点过头了,并且违反了TwoWay数据绑定的目的。


您可以在保存之前将焦点设置在其他位置吗?

您可以通过在UI元素上调用focus()来实现。

您可以专注于任何调用"保存"的元素。如果触发器是LostFocus,则必须将焦点移到某个位置。 Save的优点是它不会被修改,并且对用户有意义。


我正在使用BindingGroup。

XAML:

1
2
3
4
5
6
7
8
<R:RibbonWindow Closing="RibbonWindow_Closing" ...>

    <FrameworkElement.BindingGroup>
        <BindingGroup />
    </FrameworkElement.BindingGroup>

    ...
</R:RibbonWindow>

C#

1
2
3
4
5
6
7
8
9
10
11
12
private void RibbonWindow_Closing(object sender, CancelEventArgs e) {
    e.Cancel = !NeedSave();
}

bool NeedSave() {
    BindingGroup.CommitEdit();

    // Insert your business code to check modifications.

    // return true; if Saved/DontSave/NotChanged
    // return false; if Cancel
}

它应该工作。


由于我注意到此问题仍然很难以一种通用的方式解决,因此我尝试了各种解决方案。

最终为我解决了一个问题:
每当需要更改UI验证并将其更新到源时(关闭窗口,执行Save操作等检查更改),我都会调用一个验证函数来执行各种操作:
-确保焦点所在的元素(如文本框,组合框等)失去焦点,这将触发默认的updatesource行为
-验证DependencyObject树中提供给验证功能的所有控件
-将焦点重新设置为原始焦点元素

如果一切正常(验证成功),该函数本身将返回true->您的原始操作(通过可选的询问确认关闭,保存等)可以继续。否则,该函数将返回false,并且您的操作无法继续,因为一个或多个元素上存在验证错误(借助于元素上的通用ErrorTemplate)。

代码(验证功能基于"检测WPF验证错误"一文):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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;
    }
}

你怎么看待这件事?我相信我已经找到了一种使用反射使它更通用的方法。我真的不喜欢像其他示例一样维护列表的想法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
        }
    }
}

看到任何问题吗?


最简单的方法是将焦点设置在某个位置。
您可以立即将焦点重新设置,但是在任何地方设置焦点都会在任何类型的控件上触发LostFocus-Event并使其更新其内容:

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

另一种方法是获取焦点元素,从焦点元素获取绑定元素,然后手动触发更新。 TextBox和ComboBox的示例(您需要添加需要支持的任何控件类型):

1
2
3
4
5
6
7
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();


推荐阅读