假设我有一个带有文本框和确定/取消按钮的模态对话框。它建立在MVVM上 - 即它有一个ViewModel对象,该对象具有文本框绑定的字符串属性。
说,我在文本框中输入一些文本然后抓住我的鼠标并单击“确定”。一切正常:在单击时,文本框失去焦点,这会导致绑定引擎更新ViewModel的属性。我得到了我的数据,每个人都很开心。
现在假设我不使用鼠标。相反,我只是按下键盘上的Enter
。这也会使“确定”按钮“点击”,因为它被标记为IsDefault="True"
。但猜猜怎么了?在这种情况下,文本框没有不失去焦点,因此,绑定引擎仍然无辜地无知,我不会得到我的数据。荡!
同一场景的另一种变体:假设我在主窗口中有一个数据输入表单,在其中输入一些数据,然后点击Ctrl+S
进行“保存”。你猜怎么着?我的最新条目没有得到保存!
使用UpdateSourceTrigger=PropertyChanged
可以稍微补救,但这并非总是可行。
一个明显的例子是StringFormat
使用绑定 - 当我试图输入时,文本会一直跳回“格式化”状态。
另一个我遇到的情况是,当我在viewmodel的属性设置器中进行一些耗时的处理时,我只想在用户“完成”输入文本时执行它。
这似乎是一个永恒的问题:我记得自从我开始使用交互式界面以来,从很久以前就试图系统地解决它,但我从来没有成功过。在过去,我总是最终使用某种黑客 - 例如,向每个“演示者”(如“MVP”)添加“EnsureDataSaved”方法,并在“关键”点或类似的地方调用它。 ..
但是,凭借WPF的所有酷炫技术以及空洞的炒作,我预计他们会想出一些好的解决方案。
答案 0 :(得分:3)
在关键点,您可以强制绑定推送到您的视图模型:
var textBox = Keyboard.FocusedElement as TextBox;
BindingOperations.GetBindingExpression(textBox, TextBox.TextProperty).UpdateSource();
修改强>
好的,既然你不想要黑客,我们就必须面对丑陋的事实:
我们可以使用的类比是文本编辑器。如果应用程序是绑定到磁盘上文件的巨型文本框,则每次按键都会导致写入整个文件。甚至不需要保存的概念。这是正确的,但非常低效。我们都会立即看到视图模型需要为要绑定的视图公开缓冲区,这会在视图模型中重新引入save和强制状态处理的概念。
然而,我们发现这仍然不够有效。对于中等大小的文件,在每次击键时更新整个文件缓冲区的开销变得无法忍受。接下来,我们在视图模型中公开命令以有效地操作缓冲区,从不实际与视图交换整个缓冲区。
因此,我们得出结论,为了实现纯MVVM的效率和响应能力,我们需要公开一个有效的视图模型。这意味着所有文本框都可以绑定到没有不良影响的属性。 但,这也意味着您必须将状态向下推入视图模型才能处理。这没关系,因为视图模型不是模型;它的工作是处理视图的需求。
我们可以通过利用绑定焦点更改等快捷方式快速构建用户界面原型。但是对焦点变化的约束可能会对实际应用产生负面影响,如果是这样,那么我们就不应该使用它。
有什么替代方案?为频繁更新公开酒店。调用它与调用旧的低效属性相同。使用具有取决于视图模型状态的逻辑的slow属性来实现快速属性。视图模型获取save命令。它知道快速房产是否已被推迟到缓慢房产。它可以决定慢属性何时何地与模型同步。
但是你说,我们不是刚刚将视图中的黑客移动到视图模型中吗?不,我们已经失去了一些优雅和简洁,但回到文本编辑器的比喻。我们有来解决问题,解决它就是视图模型的工作。
如果我们想要使用纯MVVM并且我们想要效率和响应性,那么蹩脚的启发式方法就像让我们避免更新绑定源直到元素失去焦点将无济于事。他们引入了尽可能多的问题。在这种情况下,我们应该让视图模型完成它的工作,即使这意味着增加了复杂性。
假设我们接受它,我们如何管理复杂性?我们可以实现一个通用的包装器实用程序类来缓冲slow属性,并允许视图模型挂钩其get和set方法。我们的实用程序类可以自动注册save命令事件,以减少视图模型中的样板代码量。
如果我们做得对,那么视图模型的所有快速足以与属性更改绑定一起使用的部分仍将是相同的,而其他部分值得提出问题“这个属性是否也是慢?”将有少量代码来解决这个问题,而且视图并不明智。
答案 1 :(得分:0)
这是一个棘手的问题,我同意应该找到一个非hack和或多或少的无代码解决方案。以下是我的想法:
所以目前我看到的唯一“好”的解决方案需要视图开发人员遵守规则;以特定方式设置绑定或使用特殊按钮。
答案 2 :(得分:0)
问题是TextBox的文本具有LostFocus的默认源触发器而不是PropertyChanged。恕我直言,这是一个错误的默认选择,因为它是非常意外的并且可能导致各种各样的问题(例如你描述的问题)。
不幸的是,似乎某些场景对于TextBox来说仍然有点问题,因此需要一些解决方法。例如,请参阅my question。您可能希望针对特定问题打开Connect错误(或两个)。
编辑: 按住Ctrl + S并将焦点放在TextBox上,我会说行为是正确的。毕竟,您正在执行命令。这与当前(键盘)焦点无关。 该命令甚至可能依赖于聚焦元素!您没有点击按钮或类似按钮,这会导致焦点发生变化(但是,根据按钮的不同,它可能会发出与之前相同的命令)。
因此,如果您只想在从TextBox中失去焦点时更新绑定的文本,但同时您想要使用TextBox的最新内容(即没有丢失焦点的更改)触发命令,不匹配。因此,您必须更改绑定到PropertyChanged,或手动更新绑定。
编辑#2:编辑#2: 至于你的两个案例,为什么你不能总是使用PropertyChanged:答案 3 :(得分:0)
我会为默认按钮添加Click事件处理程序。在调用命令之前执行按钮的事件处理程序,因此可以通过更改事件处理程序中的焦点来更新数据绑定。
private void Button_Click(object sender, RoutedEventArgs e) {
((Control)sender).Focus();
}
但是,我不知道类似的方法是否可以与其他shorcut键一起使用。
答案 4 :(得分:0)
是的,我有一些经验。 WPF和Silverlight仍然有他们的痛苦领域。 MVVM并没有解决所有问题;它不是一个神奇的子弹,框架中的支持越来越好,但仍然缺乏。例如,我仍然发现编辑深度子集合是一个问题。
目前我会逐个处理这些情况,因为很多情况取决于个人观点的工作方式。这是我花费大部分时间的原因,因为I generate a lot of plumbing using T4所以我还有时间留下这些怪癖。
答案 5 :(得分:0)
<强>编辑:强> 我们有一个实用命令(如转换器),需要有关具体视图的知识。此命令可以重用于具有相同错误的任何对话框。并且您只在查看存在此错误的位置添加此功能/ 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;
}
}
}
这只是想法,而不是完整的解决方案。