如何不丢失绑定源更新?

Posted

技术标签:

【中文标题】如何不丢失绑定源更新?【英文标题】:How not to lose binding source updates? 【发布时间】:2011-06-08 08:10:42 【问题描述】:

假设我有一个带有文本框和确定/取消按钮的模式对话框。它是建立在 MVVM 之上的——即它有一个 ViewModel 对象,该对象带有一个文本框绑定到的字符串属性。

假设我在文本框中输入了一些文本,然后抓住鼠标并单击“确定”。一切正常:在单击的那一刻,文本框失去焦点,这导致绑定引擎更新 ViewModel 的属性。我得到了我的数据,大家都很高兴。

现在假设我不使用鼠标。相反,我只是在键盘上点击Enter。这也会导致“确定”按钮“单击”,因为它被标记为IsDefault="True"。但猜猜怎么了?在这种情况下,文本框不会失去焦点,因此绑定引擎仍然天真无知,我没有得到我的数据。该死!

同一场景的另一个变体:假设我在主窗口中有一个数据输入表单,在其中输入一些数据,然后点击Ctrl+S 进行“保存”。你猜怎么了?我的最新条目没有保存!

这可以通过使用UpdateSourceTrigger=PropertyChanged在某种程度上解决,但这并不总是可行的。

一个明显的例子是将StringFormat 与绑定一起使用——当我尝试输入文本时,文本不断跳回到“格式化”状态。

我自己遇到的另一种情况是,当我在视图模型的属性设置器中进行一些耗时的处理时,我只想在用户“完成”输入文本时执行它。

这似乎是一个永恒的问题:我记得很久以前就尝试系统地解决它,自从我开始使用交互式界面以来,但我从未完全成功。过去,我总是最终使用某种技巧——比如,为每个“演示者”(如“MVP”)添加一个“EnsureDataSaved”方法,并在“关键”点调用它,或者类似的东西。 ..

但鉴于 WPF 的所有酷技术以及空洞的炒作,我预计他们会提出一些好的解决方案。

【问题讨论】:

【参考方案1】:

您如何看待代理命令和 KeyBinding to ENTER 键?

已编辑: 我们有一个实用命令(如转换器),它需要有关具体视图的知识。此命令可用于具有相同错误的任何对话框。并且您仅在存在此错误的情况下添加此功能/hack,并且VM将很清楚。

VM 的创建是为了使业务适应视图,并且必须提供一些特定的功能,例如数据转换、UI 命令、附加/帮助字段、通知和黑客/解决方法。如果我们在 MVVM 的各个级别之间存在泄漏,我们就会遇到以下问题:高连接性、代码重用、VM 单元测试、痛苦代码。

xaml 中的用法(按钮上没有 IsDefault):

<Window.Resources>
    <model:ButtonProxyCommand x:Key="proxyCommand"/>
</Window.Resources>

<Window.InputBindings>
    <KeyBinding Key="Enter"
          Command="Binding Source=StaticResource proxyCommand, Path=Instance" 
          CommandParameter="Binding ElementName=_okBtn"/>
</Window.InputBindings>
<StackPanel>
    <TextBox>
        <TextBox.Text>
            <Binding Path="Text"></Binding>
        </TextBox.Text>
    </TextBox>
    <Button Name="_okBtn" Command="Binding Command">Ok</Button>
</StackPanel>

这里使用了特殊的代理命令,它接收元素(CommandParameter)来移动焦点并执行。但是这个类需要 ButtonBase for CommandParameter:

public class ButtonProxyCommand : ICommand

    public bool CanExecute(object parameter)
    
        var btn = parameter as ButtonBase;

        if (btn == null || btn.Command == null)
            return false;

        return btn.Command.CanExecute(btn.CommandParameter);
    

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    
        if (parameter == null)
            return;

        var btn = parameter as ButtonBase;

        if (btn == null || btn.Command == null)
            return;

        Action a = () => btn.Focus();
        var op = Dispatcher.CurrentDispatcher.BeginInvoke(a);

        op.Wait();
        btn.Command.Execute(btn.CommandParameter);
    

    private static ButtonProxyCommand _instance = null;
    public static ButtonProxyCommand Instance
    
        get
        
            if (_instance == null)
                _instance = new ButtonProxyCommand();

            return _instance;
        
    

这只是想法,不是完整的解决方案。

【讨论】:

所以,让我确保我得到这个正确。您的想法是使用将转移焦点的代理命令,然后委托给另一个命令。并在我可能关心获取最新数据的任何地方使用这种代理命令。对吗? 是的。而这项工作。我认为将附加命令用于附加功能(Enter 键)并将其重定向到原始控制和命令是正确的。注意: KeyBinding 和 ButtonProxyCommand 在整个表单中只有一个。用于按钮的代理,而不是用于每个编辑器控件(TextBox、NumericUpDown 等)。【参考方案2】:

在关键点,您可以强制绑定推送到您的视图模型:

var textBox = Keyboard.FocusedElement as TextBox;
BindingOperations.GetBindingExpression(textBox, TextBox.TextProperty).UpdateSource();

编辑:

好的,既然你不想被黑客攻击,我们就不得不面对丑陋的事实:

为了实现干净的视图,视图模型公开的属性应该对频繁的绑定更新友好。

我们可以使用的一个类比是文本编辑器。如果应用程序是一个绑定到磁盘文件的巨大文本框,那么每次击键都会导致写入整个文件。甚至不需要储蓄的概念。这是非常正确的,但效率极低。我们都立即看到,视图模型需要公开一个缓冲区以供视图绑定,这在我们的视图模型中重新引入了保存和强制状态处理的概念。

然而,我们发现这仍然不够有效。即使是中等大小的文件,每次击键时更新整个文件缓冲区的开销也会变得难以承受。接下来,我们在视图模型中公开命令以有效地操作缓冲区,而实际上从不与视图交换整个缓冲区。

因此我们得出结论,为了使用纯 MVVM 实现效率和响应能力,我们需要公开一个高效的视图模型。这意味着所有文本框都可以绑定到没有不良影响的属性。 但是,这也意味着您必须将状态下推到视图模型中来处理它。没关系,因为视图模型不是模型;它的工作是处理视图的需求。

确实,我们可以利用快捷方式(如绑定焦点更改)来快速制作用户界面原型。但是绑定焦点更改可能会在实际应用程序中产生负面影响,如果是这样,那么我们根本不应该使用它。

有什么选择?将属性暴露给频繁更新。将其称为与旧的低效属性相同的名称。使用取决于视图模型状态的逻辑的慢速属性来实现您的快速属性。视图模型获取保存命令。它知道快速属性是否已被推送到慢速属性。它可以决定是否将慢速属性同步到模型的时间和位置。

但是你说,我们不是刚刚将 hack 从视图移到了视图模型吗?不,我们失去了一些优雅和简单,但回到文本编辑器的类比。我们必须解决问题,而解决问题是视图模型的工作。

如果我们想使用纯 MVVM 并且我们想要效率和响应能力,那么像让我们避免更新绑定源直到元素失去焦点这样的蹩脚的启发式方法将无济于事。他们引入的问题与解决的问题一样多。在这种情况下,我们应该让视图模型完成它的工作,即使这意味着增加复杂性。

假设我们接受它,我们如何管理复杂性?我们可以实现一个通用的包装实用程序类来缓冲慢速属性并允许视图模型挂钩它的 get 和 set 方法。我们的实用程序类可以自动注册保存命令事件,以减少视图模型中的样板代码量。

如果我们做对了,那么视图模型中所有足够快以与属性更改绑定一起使用的部分都将保持不变,而其他值得问的问题“这个属性也是慢?”将有少量代码来解决这个问题,并且视图并不明智。

【讨论】:

这在很多层面上都不起作用。首先,我不想使用这种技巧,因为它不可扩展或不可维护。例如,当我添加另一个“关键点”时,我必须记住所有这些 hack 存在的地方,并更新它们。其次,视图不一定具有“临界点”的知识。它们的定义可能在另一个视图或视图模型中。第三,这只是需要编写和维护的大量代码。 当然是破解!我在想你会在一个地方做这件事:在你保存之前。只有一个文本框需要这种处理。类似的技巧是保存焦点元素,将其移动到其他位置,保存,然后将其移回。 @Rick Sladkey,好吧,那可能是我的错,因为我不够清楚。关键是,我想要一个干净的解决方案。就肮脏的黑客而言 - 我可以自己想出一百万个。就叫我“黑客”吧:-) @Rick,关于您的编辑:谢谢!这正是我正在寻找的答案。您的回答很有效,因为它确实通知了我。我还没有针对特定小问题的确切解决方案,但感觉你已经向我展示了正确的看待它的方式。确实,看起来我已经太习惯于 MVVM 并且忽略了它的核心思想。再次感谢您。 提供视图详细信息(您使用的控件)来查看模型非常糟糕。因为可以为同一个视图模型使用许多不同的视图。第二个问题是在某些视图中可以使用唯一控件,而不是TextBox。是的,我同意 VM 封装视图的逻辑,但可以为该 VM 创建的所有类似视图的公共部分。【参考方案3】:

问题在于 TextBox 的文本有一个 LostFocus 的默认源触发器,而不是 PropertyChanged。恕我直言,这是一个错误的默认选择,因为它非常出乎意料并且可能导致各种问题(例如您所描述的问题)。

    最简单的解决方案是始终明确使用 UpdateSourceTrigger=PropertyChanged(正如其他人建议的那样)。 如果这不可行(无论出于何种原因),我将处理 Unloaded、Closing 或 Closed 事件并手动更新绑定(如 Rick 所示)。

不幸的是,某些情况下 TextBox 似乎仍然存在一些问题,因此需要一些变通方法。例如,请参阅my question。您可能希望针对您的特定问题打开一个(或两个)Connect 错误。

编辑: 按 Ctrl+S 并将焦点放在 TextBox 上,我会说行为是正确的。毕竟,您正在执行一个命令。这与当前(键盘)焦点无关。 该命令甚至可能取决于焦点元素! 您没有单击按钮或类似按钮,这会导致焦点发生变化(但是,根据按钮,它可能会触发与以前相同的命令)。

因此,如果您只想在 TextBox 失去焦点时更新绑定的 Text,但同时又想使用 TextBox 的最新内容触发命令(即在没有失去焦点的情况下进行更改),则不匹配。因此,您要么必须将绑定更改为 PropertyChanged,要么手动更新绑定。

编辑#2: 至于你的两种情况,为什么你不能总是使用 PropertyChanged:

    您究竟在用 StringFormat 做什么?到目前为止,在我的所有 UI 工作中,我都使用 StringFormat 重新格式化从 ViewModel 获得的数据。但是,我不确定如何将 StringFormat 与用户再次编辑的数据一起使用。我猜你想格式化显示的文本,然后“取消格式化”用户输入的文本以便在你的 ViewModel 中进一步处理。根据您的描述,它似乎并非一直都正确“未格式化”。
      打开无法正常工作的 Connect 错误。 编写您自己在绑定中使用的 ValueConverter。 拥有一个带有最后一个“有效”值的单独属性,并在您的 ViewModel 中使用该值;仅当您从数据绑定中使用的属性中获得另一个“有效”值时才更新它。
    如果您有一个长时间运行的属性设置器(例如“验证”步骤),我会在一个单独的方法中执行长时间运行的部分(获取器和设置器通常应该相对“快速”)。然后在工作线程/线程池/BackgroundWorker 中运行该方法(使其可中止,以便在用户输入更多数据后使用新值重新启动它)或类似方法。

【讨论】:

我想知道我会在错误中写什么。这似乎有点像设计。只是设计有缺陷,但我真的不能说有什么问题…… @Fyodor:错误:当通过 Enter 键关闭表单时(您是否还通过 Escape 检查取消?),永远不会触发 TextBox 上的 LostFocus 绑定。对于第二部分,请参阅我的编辑。 关于您的“编辑”:这就是我问题的重点!我明白,当我按下 Ctrl+S 时,更新源没有“技术”原因。另一方面,这种行为是用户所期望的,不是吗?这正是我的问题的本质:如何在 WPF 中解决这个问题?而且我不只是在寻找一种一次性的黑客式解决方案。如果您愿意,我正在寻找一种设计模式的通用方法。 @Fyodor:当前更新绑定时存在三种可能性:PropertyChanged、LostFocus、Explicit。这三个工作如广告和恕我直言,是“用户”(或者更确切地说是开发人员)所期望的。你想要的是第四种可能性:LostFocusOrCommandExecuting。不幸的是,这不存在。所以要么你必须自己做(通过将它保持在 LostFocus 并在命令触发时手动更新绑定)或者你切换到 PropertyChanged。 @Fyodor 查看我关于为什么不能使用 PropertyChanged 的​​编辑。【参考方案4】:

是的,我有很多经验。 WPF 和 Silverlight 仍然有它们的痛点。 MVVM 并不能解决所有问题。它不是灵丹妙药,框架中的支持越来越好,但仍然缺乏。例如,我仍然发现编辑深层子集合是个问题。

目前我会逐案处理这些情况,因为很大程度上取决于个人视图的工作方式。这是我大部分时间都花在这上面的,因为I generate a lot of plumbing using T4 所以我有时间来处理这些怪癖。

【讨论】:

【参考方案5】:

我将为默认按钮添加一个 Click 事件处理程序。按钮的事件处理程序在调用命令之前执行,因此可以通过更改事件处理程序中的焦点来更新数据绑定。

private void Button_Click(object sender, RoutedEventArgs e) 
    ((Control)sender).Focus();

但是,我不知道类似的方法是否可以与其他快捷键一起使用。

【讨论】:

再次,一次性破解。不可维护,不可扩展,不直观。并且没有解决另一种情况 - 按“Ctrl+S”的情况。【参考方案6】:

这是一个棘手的问题,我同意应该找到一个非 hack 且或多或少无代码的解决方案。以下是我的想法:

    视图负责,因为它将 IsDefault 设置为 true 并允许此“问题” ViewModel 不应以任何方式负责修复此问题,它可能会引入从 VM 到 V 的依赖关系,从而破坏模式。 无需向视图添加 (C#) 代码,您所能做的就是更改绑定(例如更改为 UpdateSourceTrigger=PropertyChanged)或向按钮的基类添加代码。在按钮的基类中,您可以在执行命令之前将焦点转移到按钮上。仍然是 hack-ish,但比向 VM 添加代码更干净。

所以目前我看到的唯一“不错”的解决方案要求视图开发人员遵守规则;以特定方式设置绑定或使用特殊按钮。

【讨论】:

那么按“Ctrl+S”的情况呢? 更改绑定可以解决我认为的问题。覆盖按钮不会解决它。 在原帖中,我提供了两个可能的原因(这两个原因都存在于我当前的项目中)导致无法使用UpdateSourceTrigger=PropertyChanged 如果您只想在输入所有输入后写入 VM,您需要恢复为 UpdateSourceTrigger=Explicit,实际上,通过编写代码来处理事件。顺便说一句:如果需要设置这么长时间的属性,我怀疑属性设置器应该是一个函数。抱歉,我认为这是不可能的。 您有丰富的用户界面设计经验吗?如果是这样,您通常是如何解决这个问题的?你真的总是为每一种情况发明一个一次性的黑客吗?我主要是在等待 GUI 专业人士的一些答案,他们每天都在处理这些事情,并且有一些完善的处理方式。

以上是关于如何不丢失绑定源更新?的主要内容,如果未能解决你的问题,请参考以下文章

EasyNVR分屏切换时视频源丢失问题的优化分享

如何在不丢失 docker 数据的情况下更新 prometheus 配置文件

WPF Datagrid:更新太频繁 - 选择丢失

如何更新详细信息表中的数据并且不丢失范围选择和过滤器?

1) 获取数据 2) 订阅更新。使用 pub sub 时如何不丢失 1 和 2 之间发生的更新?

如何检索由具有不正确 CoreData 架构的更新 iOS 应用程序导致的数据丢失