65.9K
CodeProject 正在变化。 阅读更多。
Home

避免 UpdateData

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.43/5 (29投票s)

2000年5月17日

viewsIcon

263065

了解如何在模态对话框中避免使用 UpdateData。

引言

Microsoft 并未充分记录与控件协同工作的正确方法。这留给程序员自行摸索,不知如何神奇地推断出正确的方法。不幸的是,所有证据都表明 UpdateData 是处理对话框中访问控件信息问题的正确方法。这是不正确的。实际上,使用它很危险。这完全是错误的做法。

在我发布这篇文章后,一位读者 Doug Harrison 向我发来了批评。我将他的评论 放在文末,因为其中一些指出了我方法中潜在的缺陷,并且是有效的观点。我也有几点不同意,我会告诉你原因,你可以自行判断你想怎么做。但还是要感谢 Doug,他花时间给我发了消息。

您永远不应该在模态对话框中调用 UpdateData。就是不要这么做。永远不要。原因有很多。一个原因是 OnOK 处理程序调用 UpdateData 来将控件值存储在关联的成员变量中。OnCancel 处理程序不调用 UpdateData。因此,假设的行为,或应该假定的行为是,如果您在调用 DoModal 之前设置了一组成员变量,则在对话框成功完成后,成员变量将包含新的控件值,而在错误完成后,成员变量将保持您放入它们的值。这使得调用模态对话框非常简单。

CMyDialog dlg;
dlg.m_Count = somecounter;
dlg.m_Text = sometext;
dlg.m_Option = someBool;
dlg.DoModal( );
somecounter = dlg.m_Count;
sometext = dlg.m_Text;
someBool = dlg.m_Option;

没什么大不了的!但如果您自己调用 UpdateData,这种简单的范例将不再有效。因为如果用户点击 Cancel,您已经将值弄乱了,代表了用户刚刚选择拒绝的某个中间状态。

Doug Harrison 向我发送了一份 批评,指出上述序列中存在一个严重缺陷。这是我从未发现过的缺陷,因为我从未用过对话框中的某个特定功能(好吧,我用过,但不喜欢,并且选择故意避开它)。但他的批评是很有道理的,您应该阅读它。我已将其放在文末。

避免使用 UpdateData 的另一个原因是 UpdateData 会恢复所有状态。这意味着您必须记住保存该状态!我发现这是我在管理 UpdateData 的使用时遇到的最大问题,直到我弄清楚这根本就是个坏主意。您必须确保,每次发生更改时,都通过 UpdateData 将所有值复制到控件之外,这样,如果您想进行另一次更改并恢复值,您将拥有正确的集合,而不是一些先前值和当前值的混合。这让我抓狂。可能也会让您抓狂。

此外,如果您调用 UpdateData,它会将内存中的值推送到您所有的编辑控件中。这将立即导致一系列 OnChangeWM_COMMAND/EN_CHANGE)通知,因为每个编辑控件都会被返回。现在假设存在一个关于一致性检查的问题,例如修改控件 A 会影响控件 B 中的值。但如果您执行 UpdateData,您就必须处理这一系列事件可能发生的顺序与用户预期的顺序不同,并且您必须做出各种准备来处理它,例如始终设置一个标志,指示是否应对更改做出反应。

总的来说,我发现我编写的三个使用 UpdateData 的程序比我之前或之后任何一个不使用 UpdateData 的程序都要更难编写、更难调试、维护起来也更困难。对我而言,易于维护是最重要的关注点之一。低开发成本也很重要。

我发现,正确的方法是使用控件变量。如果您不理解什么是 控件变量,请单击超链接阅读我关于它们的文章。

那么,当对话框处于活动状态时,您如何获取控件的值?只需使用控件变量访问控件即可!例如,要确定是否应启用 OK 按钮,并且只有在文本值非空时才能启用,您可以这样写:

CString s;
c_Text.GetWindowText(s);
s.TrimLeft();
c_OK.EnableWindow(s.GetLength() != 0);

比记住如何调用 UpdateData(参数是 TRUE 还是 FALSE?)更容易,并且避免了弄乱 OK/Cancel 行为的风险。

上面的代码写在哪里?上面的代码写在哪里?嗯,那是 另一篇文章

好的,我稍微妥协一下,再告诉你一个不使用 UpdateData 的原因,这是我发现的唯一使用 UpdateData 的原因(继续阅读,这不是一个使句子看起来矛盾的笔误!)我有一个客户对这种情况感到不满,即您在对话框中进行了一些更改,感到困惑,然后不得不点击 Cancel 退出对话框,然后立即重新进入对话框重新开始。他坚持要求他的产品中的每个对话框都必须有一个 Reset 按钮,用于将对话框重置为其原始状态。如果您避免使用 UpdateData,这将很容易实现:您使用 UpdateData。我的意思是,如果您避免不必要的、无意义的、无用的 UpdateData 调用(即,几乎所有我能想到的对话框中的任何 UpdateData 调用),那么原始信息将保持不变,一次 UpdateData 调用将恢复所有原始值。但请注意,UpdateData唯一用途是将控件加载为原始输入值,如果在此过程中使用它在中间点取回值,则此方法将不起作用;在这种情况下,您已经破坏了输入状态。实际上,我喜欢他的风格,所以,像大多数教条主义者一样,我不得不说“但是有一个例外”。到目前为止,这是我在非数据库应用程序中发现的唯一例外(请参阅下面的 Doug Harrison 的评论)。由于在过去大约五年里,我编写了字面上数百个对话框,并且在前三个应用程序之后从未调用过 UpdateData,除了重置所有状态为原始输入状态之外,我无法对任何声称该调用是通用对话框正确方法的说法感到过于兴奋。或者说,显式调用 UpdateData 甚至在其中有任何位置,除了重置情况。

来自 Doug Harrison 的一些评论

Doug Harrison 花时间回复了我的文章。他提出了几点有趣的观点,其中一点表明我尚未用到许多对话框功能,这可能导致一些问题。我在此包含了他评论的稍作编辑的版本,以便您可以阅读并做出自己的选择。


Doug 写道:

我没有读完您的所有文章,但我对您的 OnUpdateData 文章有一些评论。

下面的序列存在一些问题

CMyDialog dlg;
dlg.m_Count = somecounter;
dlg.m_Text = sometext;
dlg.m_Option = someBool;
dlg.DoModal( );
somecounter = dlg.m_Count;
sometext = dlg.m_Text;
someBool = dlg.m_Option;

1.您不应该依赖用户直接初始化对话框成员。与几乎所有类一样,由构造函数承担此责任要好得多。

这一点我不同意。只有调用者知道这些值是什么,默认构造函数根本不可能知道调用站点的任何细节。为了避免这种情况,您必须手动修改构造函数以添加必要的参数。至少在我几年前第一次尝试时,这会严重混淆类向导,它随后会损坏源文件。这让我足够紧张,以至于我再也没有尝试过。使用构造函数的技巧,他下面会展示(而且,在我看来,这确实是更好的做法),仍然需要在之后提取数据。微软在一个虚幻的世界中工作,在这个世界里,所有这些参数都必须是简单的标量,这种限制非常愚蠢,以至于我无法想象任何人会认为它足够。我宁愿传递一个指向结构的指针,但那样我就不能使用类向导来处理值变量了,因为它无法解析结构访问,或者除了简单变量之外的任何东西。既然我仍然需要设置所有东西,然后提取回来,我更喜欢看到赋值的对称性。我的想法是有一个类向导,允许我指定一个作为引用传递的参数列表,并且该列表只在完全成功完成后(即,所有验证都通过)更新变量。在缺乏有用的东西的情况下,我认为初始化成员变量的名称是接口规范的一部分。请注意,我唯一使用 m_ 前缀的地方是用于将参数从调用者传递到对话框的成员变量;在任何其他地方,在我编写的任何对话框中,您都不会发现用于任何其他目的的 m_ 变量。所以,我只因为工具仍然原始而不同意这一点。底线是,微软在提供必要的自动化方面还有很长的路要走。在缺乏体面的自动化的情况下,我宁愿自己动手。我并非反对自动化,只是反对糟糕的自动化。--jmn

2。如果您关心这些成员在对话框返回时是否有值,那么您必须关心 DoModal 的返回值。要明白为什么,请考虑一下,如果存在验证错误,当从 OnOK 调用 UpdateData 时,它不会完成,然后用户可以随后通过取消对话框退出。在那种情况下,上面的代码可能会存储新值和旧值的混合,尽管用户取消了对话框,具体取决于 DDV 调用在 DoDataExchange 中失败的位置,以及 DDX/DDV 调用的顺序。

因此,上面的代码最好这样写:

CMyDialog dlg(somecounter,sometext,someBool);
if (dlg.DoModal() == IDOK)
   {
    // User said OK, and validation succeeded, 
    //so update application state
   }

在这一点上,他是绝对正确的。因为我出于我认为合理的理由从不使用 DDV,所以我永远不会遇到这种情况。如果您确实使用 DDV,或者认为将来可能会使用 DDV,那么您需要使用他在此处显示的更正式正确的格式。--jmn

另外,我无法同意“永远不要调用 UpdateData”的前提。该函数肯定有其用途,即使在模态对话框中。例如,考虑一个对话框,其中(部分)包含一些控件,它们提供对简单数据库(键,项)对的接口,例如 STL 映射。有一个 Save 按钮,按下它就可以保存当前记录。

我必须承认,我从没想过要这样做;我倾向于明确地编写代码,以便我能了解发生了什么。这不是“我不信任自动化,我是个真正的程序员”。而是“自动化生成的代码过于简单,无法满足我的需求”。但如果您想这样做,那么问题在于对话框本身,而不是调用者,关心状态,这是一个有效的观点。--jmn

3. 要自己编写 Save 函数,您需要填充对话框数据成员,并为此使用 UpdateData。如果您直接访问控件,正如您建议的那样,您会绕过必要的对话框数据验证,并且会使您的代码因为大量控件查询而变得复杂。直接访问控件只有在您不关心对话框数据不反映控件当前状态的情况下才有效。

我同意,如果您正在使用 DDV。因为我认为 DDV 的基本设计存在严重缺陷(它只在您点击 OK 时进行验证,而且我不喜欢它的工作方式;我更喜欢进行连续验证,根据数据有效性的变化来启用/禁用 OK,甚至在状态行或其他位置显示缺失、不正确或不一致的内容。所以我故意选择避免 DDV 机制,而倾向于一种在我看来更用户友好的系统。请查看我关于 对话框状态维护 和实时即时响应 验证机制 的文章。如果您选择使用 DDV,那么他关于绕过检查的观点是正确的。--jmn

4. 您需要重写 OnOK,因为您想给用户一个保存任何更改的机会。所以,您编写一个 QuerySave 函数,该函数在用户给出 OK 时调用 Save。总之,如果您重写 OnOK,您的重写必须调用 UpdateData。在此数据库示例中,重写 OnCancel 并让它也通过 QuerySave 位,以便 OnCancel 在某些情况下也可以调用 UpdateData,这是恰当的。

如果您在一个模型中工作,在该模型中点击 OK 后会询问“保存更改?”并需要确认,我也同意。我一直采用的模型是,如果用户点击 OK,其意图就是保存任何更改,如果用户不想保存更改,用户会点击“Cancel”。如果您另外重写 OnOK,您可以调用 CDialog::OnOK,它会隐式调用 UpdateData,因为调用 CDialog::OnOK 最终会调用虚拟方法。--jmn

附注:当您重写 OnOK 时,如果您的 UpdateData 调用和其他验证成功,结束对话框的正确方法是调用 EndDialog,通常是 EndDialog(IDOK),而不是调用 CDialog::OnOK。这避免了无意义的第二次 UpdateData 调用。

是的,如果选择使用这种样式,这是正确的。由于我选择不使用这种样式,对我来说一直不是问题。我有一个担忧是,通过调用 EndDialog,您声明您知道 CDialog::OnOK 的实现只是 UpdateData 后跟 EndDialog。这倾向于违反继承抽象的观念,这让我很烦恼。特别是,如果您以后从对话框派生(这是微软根本不支持的另一个功能),那么基类可能就是执行此操作的类,并且在子类中显式调用 EndDialog 会绕过基类方法并产生不正确的结果。但他的说法是正确的,至少在此版本的 MFC 中是如此。--jmn


这些文章中表达的观点是作者的观点,不代表,也不被微软认可。

发送邮件至newcomer@flounder.com提出关于本文的问题或评论。
版权所有 © 1999 CompanyLongName 保留所有权利
www.flounder.com/mvp_tips.htm
© . All rights reserved.