Silverlight 未保存数据检测






4.84/5 (16投票s)
检测用户是否未保存更改,并弹出窗口允许他们停止离开页面(使用 ViewModel / MVVM)

保护您的用户免于丢失未保存的更改
实时示例: http://silverlight.adefwebserver.com/UnsavedDataDetection
使用 Silverlight 构建业务应用程序的一个优点是,用户可以输入大量信息,而不必担心页面“超时”。但是,如果他们输入了大量信息,却意外地离开了页面,或者意外地关闭了 Web 浏览器,那么他们就会丢失所有未保存的更改。
本文介绍了一种弹出窗口的方法,该窗口可以让用户有机会保存任何未保存的更改。
示例应用程序

加载应用程序时,您会看到示例信息。保存按钮是禁用的,ISDirty 复选框是未选中的。

如果您进行更改并按下Tab键,保存按钮现在会启用,ISDirty 复选框现在会选中。

如果您试图在表单“脏”时离开页面,您将看到一个弹出窗口,指示未保存的更改数量,并询问您是想继续离开页面,还是想停留并修复所有未保存的更改。

如果您单击保存按钮,保存按钮将禁用,ISDirty
复选框将取消选中。
您现在可以离开页面,或者关闭 Web 浏览器,而不会看到任何警告。
LightSwitch 的实现方式
Microsoft 的 LightSwitch 程序内置了此功能。这就是它使用的 JavaScript
function checkDirty(e) {
var needConform = false;
var message = 'You may lose all unsaved data in the application.'; // default message
var silverlightControl = document.getElementById("SilverlightApplication").Content;
if (silverlightControl) {
var applicationState = silverlightControl.ApplicationState;
if (applicationState) {
if (applicationState.IsDirty) {
needConform = true;
message = applicationState.Message;
}
}
else {
needConform = true;
}
}
if (needConform) {
if (!e) e = window.event;
e.returnValue = message;
// IE
e.cancelBubble = true;
//e.stopPropagation works in Firefox.
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
// Chrome
return message;
}
}
window.onbeforeunload = checkDirty;
我感到很惊讶,因为这就是全部了。其他所有内容都隐藏在 LightSwitch
程序中,而 Microsoft 没有共享任何代码。我决定让我的版本使用他们的 JavaScript,因为我认为他们花了很多钱聘请了最优秀、最聪明的人来编写它。
关于如何做到这一点的信息出乎意料地少。我只找到一个由Daniel Vaughan 提供的例子,当浏览器关闭时从 Silverlight 调用 Web 服务,该例子像 LightSwitch
那样弹出窗口。然而,他的例子还包含了更多内容,例如调用 Web 服务,这仍然需要我创建自己的实现。不过,他的例子确实向我展示了如何做到这一点。
ApplicationState 类
我需要实现的基本功能是
- 检测属性何时更改(即脏)
- 检测属性何时恢复到原始值(即不再脏)
- 允许重置所有属性为非脏状态(例如,当按下保存按钮时)
这是执行此操作的类
namespace UnsavedDataDetection
{
public class ApplicationState
{
// Properties
#region IsDirty
[ScriptableMember]
public bool IsDirty
{
get
{
// Return bool if there are Dirty Elements
return (Elements.Where(x => x.IsDirty == true).Count() > 0);
}
}
#endregion
#region Message
[ScriptableMember]
public string Message
{
get
{
// Return a message indicating how many Dirty Elements there are
return string.Format("There are {0} unsaved changes",
Elements.Where(x => x.IsDirty == true).Count().ToString());
}
}
#endregion
// Methods
#region AddElement
public void AddElement(ApplicationElement paramElementName)
{
// Do we already have the Element?
var CurrentElement = (from Element in Elements
where Element.ElementKey == paramElementName.ElementKey
select Element).FirstOrDefault();
if (CurrentElement == null)
{
// Ensure that the Element has been marked not Dirty
paramElementName.IsDirty = false;
// Set the Initial Value
paramElementName.ElementInitialValue =
paramElementName.ElementCurrentValue;
// Add the element
Elements.Add(paramElementName);
}
else
{
// Update the element
CurrentElement.ElementCurrentValue =
paramElementName.ElementCurrentValue;
// Set IsDirty
CurrentElement.IsDirty = (CurrentElement.ElementCurrentValue
!= CurrentElement.ElementInitialValue);
}
}
#endregion
#region ClearIsDirty
public void ClearIsDirty()
{
// Clear all the ISDirty flags
foreach (var item in Elements)
{
item.ElementInitialValue = item.ElementCurrentValue;
item.IsDirty = false;
}
}
#endregion
// Collections
#region Elements
private List<ApplicationElement> _Elements = new List<ApplicationElement>();
public List<ApplicationElement> Elements
{
get { return _Elements; }
set
{
if (Elements == value)
{
return;
}
_Elements = value;
}
}
#endregion
}
#region ApplicationElement
public class ApplicationElement
{
public string ElementKey { get; set; }
public string ElementName { get; set; }
public string ElementCurrentValue { get; set; }
public string ElementInitialValue { get; set; }
public bool IsDirty { get; set; }
}
#endregion
}
请注意,一些属性被标记为 [ScriptableMember]
,以便 **JavaScript** 可以调用它们。
在应用程序中注册它
ApplicationState
类需要在应用程序级别实例化并调用。我们打开 App.xaml.cs 文件,并添加以下代码
#region ApplicationState
private ApplicationState _objApplicationState = new ApplicationState();
public ApplicationState objApplicationState
{
get { return _objApplicationState; }
set
{
if (objApplicationState == value)
{
return;
}
_objApplicationState = value;
}
}
#endregion
我们还将此添加到应用程序类的构造函数中
HtmlPage.RegisterScriptableObject("ApplicationState", objApplicationState);
这允许 **JavaScript** 访问 ApplicationState
类中的 IsDirty
和 Message
属性。
实现
最后一步是在应用程序的每个页面中实现此功能。本质上,我们需要将任何更改的属性注册到 ApplicationState
类,它将完成其余的工作。
首先,我们从一个基本的 ViewModel
开始
public class HomeViewModel : INotifyPropertyChanged
{
public HomeViewModel()
{
// Set default values
FullName = "John Doe";
Email = "JohnDoe@Whitehouse.gov";
}
// Properties
#region IsDirty
private bool _IsDirty;
public bool IsDirty
{
get { return _IsDirty; }
set
{
if (IsDirty == value)
{
return;
}
_IsDirty = value;
this.NotifyPropertyChanged("IsDirty");
}
}
#endregion
#region FullName
private string _FullName;
public string FullName
{
get { return _FullName; }
set
{
if (FullName == value)
{
return;
}
_FullName = value;
this.NotifyPropertyChanged("FullName");
}
}
#endregion
#region Email
private string _Email;
public string Email
{
get { return _Email; }
set
{
if (Email == value)
{
return;
}
_Email = value;
this.NotifyPropertyChanged("Email");
}
}
#endregion
// Utility
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
我们在构造函数中添加一个 PropertyChanged 处理程序,当任何属性更改时都会触发该处理程序
// Wire-up property changed event handler
PropertyChanged += new PropertyChangedEventHandler(HomeViewModel_PropertyChanged);
方法的实现如下
#region HomeViewModel_PropertyChanged
void HomeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Run this method for any property other than the IsDirty property
// otherwise you will be in in infinite loop
if (e.PropertyName != "IsDirty")
{
// Create a new ApplicationElement
ApplicationElement objApplicationElement = new ApplicationElement();
objApplicationElement.ElementKey =
string.Format("HomeViewModel_{0}", e.PropertyName);
objApplicationElement.ElementName = e.PropertyName;
// Set ElementCurrentValue
PropertyInfo pi = this.GetType().GetProperty(e.PropertyName);
objApplicationElement.ElementCurrentValue =
Convert.ToString(pi.GetValue(this, null));
// Get an instance of the App class
App AppObj = (App)App.Current;
// Add the ApplicationElement to the objApplicationState object
AppObj.objApplicationState.AddElement(objApplicationElement);
// Set IsDirty
IsDirty = (AppObj.objApplicationState.Elements.Where
(x => x.IsDirty == true).Count() > 0);
}
}
#endregion
请注意,ElementKey
使用的是 “HomeViewModel_{0}
”。您可以将 “HomeViewModel
” 替换为当前页面的名称,以便轻松跟踪多个页面。
我们还添加了这个 Save
命令,它将清除所有 IsDirty
标志
#region SaveCommand
public ICommand SaveCommand { get; set; }
public void Save(object param)
{
// Clear IsDirty Flag
// (normally you would actually perform a save first)
// Get an instance of the App class
App AppObj = (App)App.Current;
// Clear all the ISDirty flags
AppObj.objApplicationState.ClearIsDirty();
// Set IsDirty on this class
IsDirty = false;
}
private bool CanSave(object param)
{
// Only enable if form is Dirty
return (IsDirty);
}
#endregion
用户界面 (视图)

上图显示了 UI 如何绑定到 ViewModel
。
集合 (DataGrid)
这不处理集合。在使用 DataGrid
等控件时,它会自动跟踪 DataGrid
是否脏。我建议挂钩到该属性,而不是尝试使用 ApplicationState
类来跟踪 DataGrid
中的更改。
延伸阅读
- 当浏览器关闭时从 Silverlight 调用 Web 服务
http://danielvaughan.orpius.com/category/WCF.aspx - JavaScript 与 Silverlight 之间的通信
http://blogs.silverlight.net/blogs/msnow/archive/2008/07/08/tip-of-the-day-15-communicating-between-javascript-amp-silverlight.aspx - Silverlight 和 WPF 应用程序的干净关闭
http://blog.galasoft.ch/archive/2009/10/18/clean-shutdown-in-silverlight-and-wpf-applications.aspx