ASP.NET 通用网页类库 - 第二部分






4.71/5 (21投票s)
在 ASP.NET Web Forms 中检测数据控件的更改。
目录
引言
这是关于我开发的 ASP.NET 应用程序类库系列文章的第二篇。它包含了一套通用的、可重用的页面类,这些类可以直接在 Web 应用程序中使用,以提供一致的外观、感觉和功能集。也可以从它们派生出新类来扩展它们的功能。所有功能都是相当模块化的,也可以提取并放入您自己的类中。有关该系列文章的完整列表以及演示应用程序和类代码,请参见 第一部分 [^]。
本文描述了 BasePage
类的数据更改检查功能。这是一个相当大的部分,因此将其从第一篇文章中分离出来,以免文章过长。它涵盖了与 BasePage
类更改检查功能相关的属性和方法,以及用于实现它的客户端代码。
数据更改检查
在富客户端应用程序中,很容易检测数据输入控件中的更改,并在用户尝试离开表单时警告他们即将丢失更改。您还可以完全控制他们离开表单或应用程序的方式。在基于 Web 的应用程序中,这更加困难。如果用户点击页面上的链接或按钮,您有可能捕获他们离开的尝试,但他们也可能通过在浏览器地址文本框中输入新 URL 来离开,他们也可能通过使用后退、前进或历史记录链接来导航离开,或者他们也可能 just 关闭浏览器。对于最终用户来说,在导航离开页面后才意识到自己没有先保存更改,这可能会很烦人。告诉他们“那你就别这么做”只会让他们恼火。
BasePage
类及其派生类提供了一组属性,这些属性允许您在用户保存更改之前捕获大多数离开 Web Form 的尝试,并询问他们是否真的想离开。启用后,它还会自动跟踪表单的脏状态。虽然它不是 100% 有效,但在大多数情况下效果相当不错。话虽如此,以下是局限性
- 这仅适用于 Internet Explorer 4+,因为它依赖于浏览器窗口对象的
OnBeforeUnload
事件。对于非 IE 浏览器,唯一提供的支持是通过BasePage.Dirty
属性进行脏状态跟踪。它仅在 IE、FireFox 和 Netscape 中进行过测试,因此其他浏览器可能仍存在此处未解决的问题。 - 如果您决定仍然离开,在某些情况下,您可能会收到关于未保存离开的两次提示。这通常发生在
DataGrid
Web 控件中的链接上(例如,“添加”命令链接按钮)。这更多的是一种烦扰,而不是什么。 - 如果活动的元素是您不想收到提示的元素(例如,保存按钮),但它获得了焦点,并且您通过执行与页面不交互的操作(如关闭浏览器、单击后退按钮等)来离开,那么它将在不提示的情况下退出。这是一个罕见的情况,但它确实会发生。
- 如果控件启用了
AutoPostBack
,并且在自动回发发生后您离开页面而未修改任何控件,则它可能会在不提示的情况下退出。此问题可以通过使用BasePage.Dirty
属性来规避。当BasePage.CheckForDataChanges
设置为true
时,客户端代码将检测更改,并且页面将自动为您跟踪脏状态。您也可以在没有客户端发生任何更改的情况下显式将其设置为true
,以便稍后用户在尝试离开页面时会收到提示(例如,单击按钮以在新记录上填充默认值)。 - 它只查看页面上的标准 HTML 控件(文本框、文本区域、复选框、单选按钮和 select 控件,如下拉列表和列表框)。由于几乎所有的 ASP.NET Web 控件都呈现为标准 HTML 控件之一,因此这不会造成任何问题。但是,如果您在 Web 页面上使用任何 ActiveX 控件进行数据输入,则它们将不会被检查。
工作原理
BasePage
类包含五个属性,它们与客户端 JavaScript 函数协同工作,以提供数据更改检查功能。
属性 | 描述 |
CheckForDataChanges |
将此布尔属性设置为 true 以启用数据更改检查。将其设置为 false (默认值)以禁用它。启用后,页面将呈现一个隐藏字段、一些额外的 JavaScript 变量和一个用于检查更改的函数。在页面加载时,该函数会绑定到窗口对象的 OnBeforeUnload 事件。它还被注册为 OnSubmit 语句,以允许自动跟踪脏状态。在 OnBeforeUnload 事件中无法更改 BP_bIsDirty 隐藏字段,因此必须在 OnSubmit 事件中进行更改。 |
Dirty |
这是一个布尔属性,您可以将其设置为 true ,以强制页面始终在保存更改之前提示用户。这在表单状态被修改(例如,禁用控件或根据下拉列表中的选择加载不同值)的回发发生时最有用。如果表单控件已发生更改,客户端代码会自动将其设置为 true 。必须将 CheckForDataChanges 属性设置为 true 才能使用此属性。仅在 Internet Explorer 中可用提示行为。对于所有其他浏览器,它只跟踪脏状态。不要忘记在保存或取消页面的编辑后将此属性设置为 false 。 |
ConfirmLeaveMessage |
当用户尝试在未保存更改的情况下离开时,此字符串属性包含将显示的消息(仅限 Internet Explorer)。如果未设置,将显示默认消息。 |
BypassPromptIds |
此属性设置为控件 ID 的字符串数组,即使发生更改也不应导致提示(例如,保存或取消按钮)。当由于具有这些 ID 之一的控件而发生回发时,即使数据已更改,也不会发生提示(仅限 Internet Explorer)。脏标记仍将得到更新。如果未设置,所有导致回发的控件都会在数据更改时导致提示。 |
SkipDataCheckIds |
此属性设置为控件 ID 的字符串数组,在检查更改数据时应忽略这些控件。例如,您可能有一个只读文本框用于显示文本或消息,该文本框在表单使用过程中会更新。由于它不属于保存的信息,因此可以忽略它,并且如果它是唯一更改的内容,则不会阻止离开页面。如果未设置,所有数据输入控件都将检查更改。 |
更改检查通常在页面首次加载时的 Page_Load
事件中启用。将 CheckForDataChanges
属性设置为 true
,并将 BypassPromptIds
属性设置为不应导致提示的控件列表(例如,保存按钮、启用了 AutoPostBack
的下拉列表等)。例如
private void Page_Load(object sender, System.EventArgs e)
{
if(Page.IsPostBack == false)
{
// Set up form for data change checking when
// first loaded.
this.CheckForDataChanges = true;
this.BypassPromptIds =
new string[] { "btnSave", "btnCancel",
"chkLimitToTeam" }
}
}
如果指定了此类控件的 ID,例如数据网格标题中的排序链接,您需要运行页面,查看源代码,并从呈现的 HTML 中获取链接控件的名称。在旁路列表中指定它们时,请确保将“$”字符更改为“:”,因为 __doPostback()
函数在数据更改检查代码获取 ID 之前会更改它们。
数据控件中的更改只能从用户开始与页面交互的点到回发发生的那一刻被检测到。如果您页面上有一些会导致回发的控件,例如更改某些控件状态但不实际保存到目前为止所做更改的按钮,您可能需要使用 Dirty
属性。这是必需的,因为页面在回发后呈现时,它不知道在回发之前原始值是什么。在此类回发的事件处理程序中将 Dirty
属性设置为 true
将确保用户在离开页面之前被提示保存更改。它所做的只是设置一个 JavaScript 函数检查的标志,如果为 true,则始终提示用户保存更改,而不管自页面呈现以来是否实际发生了任何更改。
生成客户端代码
客户端变量和脚本的呈现发生在重写的 OnPreRender()
方法中。代码相当直接,只是使用 StringBuilder
对象来格式化变量和脚本代码,然后将其注册到页面。脚本存储在程序集中作为资源,这样您就不必将它与程序集分开分发。此外,为了确保正确的表单受到代码的影响,会呈现一个变量,其中包含表单的唯一 ID。虽然 ASP.NET 将您限制为一个带有 runat='server'
属性的表单,但它允许您在页面上拥有其他没有该属性的常规 HTML 表单。通过强制使用 Web 表单的客户端名称,它不会破坏可能使用其他常规 HTML 表单的任何页面。下面的代码是类 1.1 版 .NET 的。2.0 版 .NET 仅在以下方面略有不同:使用 Page
对象的***方法来注册脚本块、提交语句和隐藏字段。
protected override void OnPreRender(EventArgs e)
{
StringBuilder sb;
string[] idList;
base.OnPreRender(e);
// If data change checking has been requested, output the
// dirty flag, exclusion arrays, confirm message, and the
// script.
if(this.CheckForDataChanges == true)
{
// Register a hidden field so that the client can pass
// back changes to the Dirty flag.
this.RegisterHiddenField("BP_bIsDirty",
this.Dirty.ToString(
CultureInfo.InvariantCulture).ToLower(
CultureInfo.InvariantCulture));
// Register an OnSubmit function call so that we can
// get the state of the Dirty flag and put it in the
// hidden field. It can't occur in the OnBeforeUnload
// event as everything has been packaged up ready for
// sending to the server and changes made in that event
// don't get sent to the server.
this.RegisterOnSubmitStatement("BP_DirtyCheck",
"BP_funCheckForChanges(true);");
// Create a script block containing the array
// declarations, the data loss message variable, the
// dirty flag, and the change checking script.
sb = new StringBuilder(
"<script type='text/javascript'>\n<!--\n" +
"var BP_arrBypassList = new Array(", 4096);
idList = this.BypassPromptIds;
if(idList != null)
{
sb.Append('\"');
sb.Append(String.Join("\",\"", idList));
sb.Append('\"');
}
sb.Append(");\nvar BP_arrSkipList = new Array(");
idList = this.SkipDataCheckIds;
if(idList != null)
{
sb.Append('\"');
sb.Append(String.Join("\",\"", idList));
sb.Append('\"');
}
sb.Append(");\nvar BP_strDataLossMsg = \"");
sb.Append(this.ConfirmLeaveMessage);
sb.Append("\";\nvar BP_strFormName = \"");
// BP_strFormName tells the script what form to use
// for the change checking.
sb.Append(this.PageForm.UniqueID);
sb.Append("\";\n//-->\n</script>\n");
// Add the reference to retrieve the script from the
// resource server handler.
sb.Append("<script type='text/javascript' src='");
sb.Append(ResSrvHandler.ResSrvHandlerPageName);
sb.Append("?Res=DataChange.js'></script>");
this.RegisterClientScriptBlock("BP_DCCJS",
sb.ToString());
}
...
}
重写 OnInit
方法以在服务器上创建页面时检索隐藏字段的值。如果启用了更改检查,它将 Dirty
属性设置为客户端在回发时确定的隐藏字段的值。如果禁用了更改检查,它将始终设置为 false
,因为该字段将不存在。
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// Retrieve the state of the Dirty flag (if used)
this.Dirty = Convert.ToBoolean(Request.Form["BP_bIsDirty"],
CultureInfo.InvariantCulture);
...
}
JavaScript 代码
JavaScript 代码是在客户端实现这一切的关键。渲染时,它由一个设置为 Dirty
属性当前值的隐藏字段、一组用 ConfirmLeaveMessage
、BypassPromptIds
和 SkipDataCheckIds
属性的值填充的变量,以及 JavaScript 函数 BP_funCheckForChanges()
和 BP_OnSubmit()
组成。脚本末尾的内联 JavaScript 代码将更改检查函数绑定到窗口对象的 OnBeforeUnload
事件。
更改检查函数还作为表单的 OnSubmit
属性的一部分进行注册,以允许它检测更改并将隐藏字段设置为 true
。客户端代码模块还将 BP_OnSubmit()
函数替换为 Web 表单的实际 Submit
方法。原始处理程序作为表单上的新 BP_RealOnSubmit
方法保存。该函数在调用更改检查函数后调用原始处理程序以更新脏状态标志。之所以需要进行这些额外的操作,是因为启用了 AutoPostBack
的控件(如下拉列表)会直接调用表单的 Submit
方法,而不会触发 OnSubmit
事件。通过替换我们自己的处理程序,我们仍然可以在这些情况下跟踪表单数据的更改。
// Replace the OnSubmit event. This is so that we can always
// update the state of the Dirty flag even when controls with
// AutoPostBack cause the submit.
document.forms.BP_RealOnSubmit =
document.forms.submit;
function BP_OnSubmit()
{
BP_funCheckForChanges(true);
// It sometimes reports an error if OnBeforeUnload
// cancels it. Ignore it.
try {
document.forms.BP_RealOnSubmit();
} catch(e) { }
}
document.forms.submit = BP_OnSubmit;
// IE Only: Hook up the event handler for OnBeforeUnload
window.onbeforeunload = BP_funCheckForChanges;
当用户尝试离开页面时,会调用更改检查函数,并执行以下步骤:
function BP_funCheckForChanges(bInOnSubmit)
{
var strID, nIdx, nNumOpts, nSkipCnt, nPos, nOptIdx;
var oElem, oOptions, oOpt, nDefSelIdx, nSelIdx;
var oForm = document.forms;
var nElemCnt = oForm.elements.length;
var bPrompt = (typeof(bInOnSubmit) == "undefined");
var ctlDirty = document.getElementsByName("BP_bIsDirty")[0];
// This prevents a double prompt that can occur when post
// back is not cancelled for a hyperlink-type control.
// Not sure why it happens but this works around it.
if(bPrompt == true && BP_bOnBeforeUnloadFired == true)
return;
// Get current state of Dirty flag
var bChanged = (ctlDirty.value == "true");
第一部分初始化函数的一些变量。bPrompt
变量根据是否向函数传递了参数而被初始设置为 true
或 false
。当传递参数时,它作为 OnSubmit
事件的一部分被调用,并且不应该提示,因为我们只关心更新脏标志的状态。当作为 OnBeforeUnload
事件的一部分被调用时,它应该提示,除非稍后由于旁路 ID 列表中的某些内容而被告知不要提示。之所以需要分开调用,是因为 OnBeforeUnload
事件发生在所有内容都已打包准备好发送到服务器之后,因此在该事件中对隐藏字段所做的更改不会被发送回。对于非 IE 浏览器,这有一个副作用,即至少有脏状态跟踪。bChanged
变量设置为 BP_bIsDirty
隐藏字段的当前值。这使得它能够在即使在什么都没有更改的回发后也能保持脏状态并提示保存更改。请注意,为了获取隐藏字段的引用,我们必须使用 document.getElementsByName
方法。这是因为页面使用 name
属性渲染注册的隐藏字段,但没有 id
属性。使用“ByName”方法可确保代码在非 IE 浏览器上正常工作。
// IE Only: The event target is most likely the item that
// caused the request to leave the page. If it's in the list
// of controls that can bypass the check, don't prompt. The
// control ID must be an exact match or must end with the name
// (i.e. it's in a DataGrid).
strID = "";
oElem = document.getElementById("__EVENTTARGET");
if(oElem == null || typeof(oElem) == "undefined" ||
oElem.value == "")
{
// Check the active element if there is no event target
if(typeof(document.activeElement) != "undefined")
{
oElem = document.activeElement
strID = oElem.id;
}
}
else
strID = oElem.value;
// Some elements may not have an ID but their parent element
// might so grab that if possible (i.e. AREA elements in a MAP
// element).
if(strID == "" && oElem != null &&
typeof(oElem) != "undefined")
{
// Link buttons in DataGrids don't have IDs but do use
// __doPostBack(). If we see a link with that in its href,
// assume __doPostBack() is running and skip the check.
// The submission will call us again.
if(oElem.tagName == "A" &&
oElem.href.indexOf("__doPostBack") != -1)
return;
if(typeof(oElem.parentElement) != "undefined")
strID = oElem.parentElement.id;
}
if(strID != "")
{
nSkipCnt = BP_arrBypassList.length;
for(nIdx = 0; nIdx < nSkipCnt; nIdx++)
if(strID == BP_arrBypassList[nIdx])
bPrompt = false;
else
{
nPos = strID.length - BP_arrBypassList[nIdx].length;
if(nPos >= 0)
if(strID.substr(nPos) == BP_arrBypassList[nIdx])
bPrompt = false;
}
}
仅限 Internet Explorer,将检查事件目标元素(导致回发的元素),看其控件 ID 是否在旁路列表中。如果是,则将 bPrompt
标志设置为 false
,以便不进行提示。例如,您不希望在单击“**保存**”按钮时提示用户保存更改。在检查控件 ID 时,它会查找精确匹配或以列表中的 ID 结尾的匹配。存在“结尾匹配”是为了处理嵌入在 DataGrid
Web 控件中的控件。它们的客户端 ID 会根据它们所在的行进行修改,以保持唯一性。由于我们不知道唯一 ID,因此基于设计时分配的 ID 的部分匹配将起作用。
// Now we'll figure out if something changed
nSkipCnt = BP_arrSkipList.length;
for(nIdx = 0; !bChanged && nIdx < nElemCnt; nIdx++)
{
oElem = oForm.elements[nIdx];
// If the control is in the list of ones to ignore,
// carry on.
for(nOptIdx = 0; nOptIdx < nSkipCnt; nOptIdx++)
{
if(oElem.id == BP_arrSkipList[nOptIdx])
break;
nPos = oElem.id.length - BP_arrSkipList[nOptIdx].length;
if(nPos >= 0)
if(oElem.id.substr(nPos) == BP_arrSkipList[nOptIdx])
break;
}
if(nOptIdx < nSkipCnt)
continue;
// Check for changes based on the control type
if(oElem.type == "text" || oElem.tagName == "TEXTAREA")
{
if(oElem.value != oElem.defaultValue)
bChanged = true;
}
else
if(oElem.type == "checkbox" || oElem.type == "radio")
{
if(oElem.checked != oElem.defaultChecked)
bChanged = true;
}
else
if(oElem.tagName == "SELECT")
{
oOptions = oElem.options;
nNumOpts = oOptions.length;
nDefSelIdx = nSelIdx = 0;
// Search for a change in the default. If
// nothing is explicitly marked as the default,
// element zero is assumed to have been the
// default.
for(nOptIdx = 0; nOptIdx < nNumOpts; nOptIdx++)
{
oOpt = oOptions[nOptIdx];
if(oOpt.defaultSelected)
nDefSelIdx = nOptIdx;
if(oOpt.selected)
nSelIdx = nOptIdx;
}
if(nDefSelIdx != nSelIdx)
bChanged = true;
}
}
接下来,将检查每个控件,看其包含的数据是否已更改。如果控件 ID 在要跳过的元素列表中,则会被忽略。这允许您在页面上拥有可以修改而不会导致提示保存更改的控件(例如,消息文本区域等)。与旁路列表一样,控件 ID 可以是精确匹配或以指定的 ID 结尾的匹配。
更改是根据控件类型检测到的。对于文本框和文本区域,会将 value
属性与 defaultValue
属性进行比较。对于复选框和单选按钮,会将 checked
属性与 defaultChecked
属性进行比较。对于 select
控件(下拉列表和列表框),将扫描集合中的每个项目。找到当前选择并将其与标记为默认选择的项目进行比较,看是否有更改。如果列表中没有项目被标记为默认值,则第一个元素被视为默认值。
if(bChanged)
{
// Pass the dirty state back to the server
ctlDirty.value = "true";
// If prompting, set the message
if(bPrompt)
{
event.returnValue = BP_strDataLossMsg;
BP_bOnBeforeUnloadFired = true;
window.setTimeout("BP_funClearIfCancelled()", 1000);
}
}
如果没有发现更改,并且 Page
类的脏标志仍然是 false
,则函数退出,不发生提示。作为 OnSubmit
事件的一部分调用时也不会发生提示。
对于 Internet Explorer,如果检测到更改,或者页面类的脏标志被设置为 true
并且它是作为 OnBeforeUnload
事件的一部分被调用的,则会将 event.returnValue
属性设置为确认消息。这会导致浏览器弹出一个消息框,询问是否可以离开页面。确认消息显示在消息框中,前面是“您确定要导航离开此页面吗?”,后面是单击确定继续或取消保留在当前页面的说明。这两条消息由浏览器添加,无法更改。只能通过 ConfirmLeaveMessage
属性修改您在中间提供的文本。
除了设置消息之外,我们还设置了一个标志变量(BP_bOnBeforeUnloadFired
),以防止在某些条件下通常会发生的双重提示。它还设置了一个超时事件,该事件调用 BP_funClearIfCancelled
函数。如果回发被取消,这有两个目的。它会清除阻止双重提示的标志,并且还会清除 __EVENTTARGET
,因为它不会在取消自动回发项然后单击按钮等情况下被清除。如果未清除,它可能会在某些使用该变量的情况下导致服务器端代码出现意外行为。
结论
我在我所有的 ASP.NET 应用程序中都使用了 BasePage
类及其派生类,以赋予它们一致的外观、感觉和功能集。尽管完全使用受限于 Internet Explorer,但数据更改检查功能在消除用户因忘记保存更改就离开页面而丢失数据这一常见抱怨方面非常有帮助。希望您会发现这个类以及类库中的其他类,或者其中的部分内容,与我一样有用。
修订历史
- 04/02/2006
- 重大更改:属性和方法名称已修改,以符合 .NET 命名约定(
BasePage.BypassPromptIds
和BasePage.SkipDataCheckIds
)。
- 重大更改:属性和方法名称已修改,以符合 .NET 命名约定(
- 11/26/2004
此版本中的更改
- 根据 Danny Dot Net 的建议进行了一些更改,以防止通过超链接类型控件回发时出现双重提示。还进行了一些其他更改,这些更改在几乎所有其他情况下也消除了双重提示。
- 修复了
__EVENTTARGET
变量在回发被取消时未被清除的潜在问题。
- 12/01/2003
- 初始发布。