Silverlight 在偶尔连接的环境中的应用
Mark Bloodworth
人们生活在联机的世界中,至少一部分人在一定时间内是这样的。在未来的某一天,也许带宽会超出我们的需要,让我们时时在线,但今天还不可能。如今,我们只能偶尔连接,偶尔拥有足够的带宽。谁也无法预料在某一时刻会发生什么样的事。
面对这样的现实,为了设计出能够带来最佳用户体验的应用程序,我们有无数的体系结构可以选择。
胖模式及瘦模式的智能客户端均有一个共同的特点,即部署在本地计算机上,因此不需要连接网络即可运行这些应用程序。另一方面,传统的基于浏览器的应用程序必须连接到远程 Web 服务器才能运行。
在这两种极端情况之间,各种选择越来越多。所有这些选择都提供不同功能让应用程序脱机运行,在 UI 设计上也有不同程度的灵活性和交互性,还提供了各种级别的安全限制。我们将讨论偶尔连接的应用程序的最新发展,这些应用程序可以提供高度互动的用户体验,并且可以在浏览器内部或外部运行。我们将提供进行网络连接检测的代码示例,以及在联机时用于上传和下载数据的后台工作程序。
背景
让我们看看本次讨论中涉及的一般应用程序的进化过程。我们的示例从只能在 Windows 操作系统上运行的简单胖客户端应用程序开始。尽管该程序允许用户脱机工作,但初始解决方案的局限性愈发明显:
- 要求支持多种操作系统。第一版只支持部分潜在用户。
- 部署问题导致用户群安装的各个版本出现差异。
由于愈发需要一个能够跨多种操作系统工作、尽量减少部署问题的轻量级解决方案,因此应用程序被重写为简单的瘦客户端 HTML 应用程序。但是,这又导致了其他一系列问题:
- 应用程序的 UI 功能受到限制,直观性降低。
- 需要长时间的浏览器兼容性测试。
- 在许多用户的网络基础设施上的性能表现不佳。例如,每次用户需要填写表单时都要下载大量参考数据,并且需要大量脚本才能提供验证中涉及的逻辑。
- 用户不能脱机使用该应用程序。
很明显,这一版本并未达标。
在这种情况下,最理想也是唯一的解决方法就是拥有直观、灵活的 UI 的富 Internet 应用程序 (RIA)。这种应用程序需要让用户在联机时管理大量数据并执行异步的数据上传和数据验证,而不需要锁定 UI。它应该支持脱机工作和访问客户端上的数据存储。它应该集成客户端上的硬件设备,例如照相机。最后,理想的解决方案应该从“开始”菜单或应用程序图标启动,而不是局限在 Web 浏览器中。
Silverlight 可以满足上述这些要求。Silverlight 3 引入了“脱离浏览器的体验”的概念,并在 Silverlight 4 中得到扩展。而且,Silverlight 4 还引入了与特定文件夹(如“我的图片”)和硬件设备(如网络摄像机)进行交互的功能。(使用此增强功能的应用程序将提示用户,该应用程序需要提升信任等级并获得用户同意才能安装。有关受信任应用程序的详细信息,请参见 msdn.microsoft.com/library/ee721083(v=VS.95)。)在本文中,我们将集中探讨构建既可联机工作又可脱机工作的应用程序体系结构时遇到的一般问题。
图 1 显示了一个粗略的体系结构,可作为候选方案。
图 1 候选的高级体系结构
本示例中典型的用户情境包括:
- 使用便携式计算机的移动员工。便携式计算机可能配备了 3G 卡,也可能连接到办公室或 Internet 热点的无线网络。
- 在连接较差的环境中使用桌面 PC 的用户,例如在老式或临时建造的办公建筑物内。
检测网络状态
需要在偶尔连接的环境中工作的应用程序必须能够检查当前的网络连接状态。Silverlight 3 通过 NetworkInterface.GetIsNetworkInterfaceAvailable 方法实现了此功能。此类应用程序还可以使用 NetworkChange.NetworkAddressChangedEvent,当网络接口的 IP 地址发生变化时,会引发此事件。
因此,让应用程序应对不稳定连接的第一步是处理 NetworkChange.NetworkAddressChangedEvent。处理此事件的明显位置是在充当 Silverlight 应用程序的入口点的 App 类中。默认情况下,该类将由 App.xaml.cs(在 C# 中)或 App.xaml.vb(在 VB.NET 中)实现。在下文中,我们将使用 C# 为例。Application_StartUp 事件处理程序看起来是订阅的合适位置:
private void Application_Startup(object sender, StartupEventArgs e)
{
NetworkChange.NetworkAddressChanged += new
NetworkAddressChangedEventHandler(NetworkChange_
NetworkAddressChanged);
this.RootVisual = newMainPage();
}
我们需要以下 using 语句:
using System.Net.NetworkInformation;
NetworkChange_NetworkAddressChanged 事件处理程序包含网络检测的主要内容。以下是实现示例:
void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
this.isConnected = (NetworkInterface.GetIsNetworkAvailable());
ConnectionStatusChangedHandler handler =
this.ConnectionStatusChangedEvent;
if (handler != null)
{
handler(this.isConnected);
}
}
第一个调用的目标是 GetIsNetworkAvailable,用于查看是否存在网络连接。在此示例中,结果存储在通过属性功能的字段中,并触发一个事件以供应用程序的其他部分使用:
private bool isConnected = (NetworkInterface.GetIsNetworkAvailable());
public event ConnectionStatusChangedHandlerConnectionStatusChangedEvent;
public bool IsConnected
{
get
{
return isConnected;
}
}
此示例是一个用于检测和处理当前网络连接的框架。但是,即使由于计算机连接到网络(不是环回或隧道接口)而使得 GetIsNetworkAvailable 返回 true,网络也可能已连接却不可用。当计算机连接到路由器而路由器已断开其 Internet 连接,或者当计算机连接到公共 Wi-Fi 接入点但需要用户通过浏览器进行登录时,就会出现这种情况。
知道存在有效的网络连接只是可靠的解决方案的一部分。Silverlight 应用程序使用的 Web 服务也可能由于多种原因而无法使用,因此偶尔连接的应用程序能够处理这种不测事件同样重要。
检查 Web 服务是否可用有多种方法。对于第一方 Web 服务(即,由 Silverlight 应用程序开发人员控制的 Web 服务),需要添加一个简单的 no-op 方法,用于定期确定服务的可用性。在此方法不可行(例如,使用了第三方 Web 服务)或不适合的情况下,应正确配置和处理超时。Silverlight 3 使用 Windows Communication Foundation 客户端配置的子集,可以在使用“添加服务引用”工具时自动生成该子集。
保存数据
除了可以对网络环境的变化做出反应外,应用程序还需要处理在应用程序脱机时输入的数据。Microsoft Sync Framework (msdn.microsoft.com/sync) 是一个全面的平台,可支持多种数据类型、数据存储、协议和拓扑。在撰写本文时,它还不能用于 Silverlight,但将来一定可以。有关详细信息,请观看位于 live.visitmix.com/MIX10/Sessions/SVC10 的 MIX10 课程或阅读位于 blogs.msdn.com/sync/archive/2009/12/14/offline-capable-applications-using-silverlight-and-sync-framework.aspx 的博客文章。显然,在 Microsoft Sync Framework 可用于 Silverlight 之后,它就是一个最佳选择。但在此之前,我们需要一个简单的解决方案来弥补缺憾。
可见队列
理想情况下,UI 元素无需关心数据是存储在本地还是存储在云中,除非有必要向用户指明应用程序目前处于脱机状态还是联机状态。要将 UI 与数据存储代码分离,使用队列是一种好办法。处理队列的组件需要能够对入队的新数据做出反应。所有这些因素都要求使用可见队列。图 2 显示了可见队列的实现示例。
图 2 可见队列
public delegate void ItemAddedEventHandler();
public class ObservableQueue<T>
{
private readonly Queue<T> queue = new Queue<T>();
public event ItemAddedEventHandler ItemAddedEvent;
public void Enqueue(T item)
{
this.queue.Enqueue(item);
ItemAddedEventHandler handler = this.ItemAddedEvent;
if (handler != null)
{
handler();
}
}
public T Peek()
{
return this.queue.Peek();
}
public T Dequeue()
{
return this.queue.Dequeue();
}
public ArrayToArray()
{
return this.queue.ToArray();
}
public int Count
{
get
{
return this.queue.Count;
}
}
}
这个简单的类包装了标准队列,并在添加数据时引发事件。它是一个泛型类,不会假设即将添加的数据的格式或类型。可见队列只在其他对象观测它时有用。在本例中,这个其他对象是名为 QueueProcessor 的类。在查看 QueueProcessor 的代码之前,还需要考虑一个问题:后台处理。当 QueueProcessor 注意到已有新数据添加到队列时,它应该在后台线程上处理数据,以使 UI 仍然能够做出响应。要实现这一设计目标,最好是使用在 System.ComponentModel 命名空间中定义的 BackgroundWorker 类。
BackgroundWorker
BackgroundWorker 是在后台线程上执行操作的简便方法。它公开两个事件:ProgressChanged 和 RunWorkerCompleted。通过这两个事件可让应用程序了解 BackgroundWorker 任务的进度。调用 BackgroundWorker.RunAsync 方法后,将引发 DoWork 事件。以下是设置 BackgroundWorker 的示例:
private void SetUpBackgroundWorker()
{
backgroundWorker = new BackgroundWorker();
backgroundWorker.WorkerSupportsCancellation = true;
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
backgroundWorker.ProgressChanged += new
ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
backgroundWorker.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
}
请注意,DoWork 事件的事件处理程序中的代码应该定期检查是否有未决的取消。有关详细信息,请参见位于 msdn.microsoft.com/library/system.componentmodel.backgroundworker.dowork%28VS.95%29 的 MSDN 文档。
IWorker
为了与 ObservableQueue 的泛型本质保持一致,最好将要做的工作的定义与 BackgroundWorker 的创建和配置分开。一个简单的接口 IWorker 定义了 DoWork 的事件处理程序。事件处理程序签名中的发送方对象将是 BackgroundWorker,让实现 IWorker 接口的类可以报告进度并检查未决的取消。以下是 IWorker 的定义:
public interface IWorker
{
void Execute(object sender, DoWorkEventArgs e);
}
当不需要上述操作时,很容易完成设计并进行分隔。而创建 IWorker 接口的想法来源于实践经验。本文介绍的 ObservableQueue 无疑是针对偶尔连接的应用程序的解决方案的一部分。但是,事实证明其他任务(例如从数码相机导入照片)也可以使用 ObservableQueue 轻松地完成。例如,将照片的路径放在 ObservableQueue 上,IWorker 的实现就可以在后台处理这些图片。让 ObservableQueue 泛化并创建 IWorker 接口即可完成这些任务,同时还能解决最初出现的问题。
处理队列
QueueProcessor 类可以将 ObservableQueue 和 IWorker 实现结合在一起做一些有意义的工作。处理队列主要是指设置 BackgroundWorker,其中包括将 IWorker 的 Execute 方法设置为 BackgroundWorker.DoWork 事件的事件处理程序,以及订阅 ItemAddedEvent。图 3 显示了 QueueProcessor 的实现示例。
图 3 实现 QueueProcessor
public class QueueProcessor<T>
{
private BackgroundWorker backgroundWorker;
private readonly IWorker worker;
public QueueProcessor(ObservableQueue<T>queueToMonitor, IWorker worker)
{
((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
ConnectionStatusChangedEventHandler(QueueProcessor_
ConnectionStatusChangedEvent);
queueToMonitor.ItemAddedEvent += new
ItemAddedEventHandler(PendingData_ItemAddedEvent);
this.worker = worker;
SetUpBackgroundWorker();
if ((((SampleCode.App)Application.Current).IsConnected) &&
(!backgroundWorker.IsBusy)
&& (((SampleCode.App)Application.Current).PendingData.Count>0))
{
backgroundWorker.RunWorkerAsync();
}
}
private void PendingData_ItemAddedEvent()
{
if ((((SampleCode.App)Application.Current).IsConnected) &&
(!backgroundWorker.IsBusy))
{
backgroundWorker.RunWorkerAsync();
}
}
private void SetUpBackgroundWorker()
{
backgroundWorker = new BackgroundWorker();
backgroundWorker.WorkerSupportsCancellation = true;
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.DoWork += new DoWorkEventHandler(this.worker.Execute);
backgroundWorker.ProgressChanged += new
ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
backgroundWorker.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
}
private void backgroundWorker_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
// Handle cancellation
}
else if (e.Error != null)
{
// Handle error
}
else
{
// Handle completion if necessary
}
}
private void backgroundWorker_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
// Raise event to notify observers
}
private void QueueProcessor_ConnectionStatusChangedEvent(bool isConnected)
{
if (isConnected)
{
if (!backgroundWorker.IsBusy)
{
backgroundWorker.RunWorkerAsync();
}
}
else
{
backgroundWorker.CancelAsync();
}
}
}
图 3 中的代码示例不包括 BackgroundWorker 的部分功能(例如处理错误)的实现,这留给读者研究。仅当应用程序目前已连接且 BackgroundWorker 并未忙于处理队列时,才会调用 RunAsync 方法。
UploadWorker
QueueProcessor 的构造函数需要 IWorker,这意味着要将数据上传到云中,就必须创建具体的实现。顾名思义,UploadWorker 正是这样的类。图 4 显示了实现示例。
图 4 将数据上传到云中
public class UploadWorker : IWorker
{
public override void Execute(object sender, DoWorkEventArgs e)
{
ObservableQueue<DataItem>pendingData =
((SampleCode.App)Application.Current).PendingData;
while (pendingData.Count>0)
{
DataItem item = pendingData.Peek();
if (SaveItem(item))
{
pendingData.Dequeue();
}
}
}
private bool SaveItem(DataItem item)
{
bool result = true;
// Upload item to webservice
return result;
}
}
在图 4 中,Execute 方法上传了已排队的条目。如果条目无法上传,则留在队列里。请注意,如果需要访问 BackgroundWorker(例如为了报告进度或检查未决的取消),则发送方对象是 BackgroundWorker。如果将结果分配给 DoWorkEventArgs 类型的 e 的 Result 属性,它将在 RunWorkerCompleted 事件处理程序中可用。
隔离存储
只要应用程序从不关闭,则将数据推入队列并在连接时将该数据提交到 Web 服务(以便存储在云中)将是很好的操作。但如果应用程序关闭,而待处理的数据仍在队列中,则需要制定策略以存储数据直到应用程序再次加载。Silverlight 提供了针对这种情况的隔离存储。
隔离存储是 Silverlight 应用程序可以使用的虚拟文件系统,可实现数据的本地存储。它能存储一定量的数据(默认限制是 1MB),但应用程序可以向用户请求更多的空间。在我们的示例中,将队列序列化到隔离存储中,可以在应用程序会话之间保持队列状态。一个名为 QueueStore 的简单类将完成此操作,如图 5 所示。
图 5 将队列序列化到隔离存储中
public class QueueStore
{
private const string KEY = "PendingQueue";
private IsolatedStorageSettings appSettings =
IsolatedStorageSettings.ApplicationSettings;
public void SaveQueue(ObservableQueue<DataItem> queue)
{
appSettings.Remove(KEY);
appSettings.Add(KEY, queue.ToArray());
appSettings.Save();
}
public ObservableQueue<DataItem>LoadQueue()
{
ObservableQueue<DataItem> result = new ObservableQueue<DataItem>();
ArraysavedArray = null;
if (appSettings.TryGetValue<Array>(KEY, out savedArray))
{
foreach (var item in savedArray)
{
result.Enqueue(item as DataItem);
}
}
return result;
}
}
如果队列中的条目是可序列化的,QueueStore 即可实现队列的保存和加载。在 App.xaml.cs 的 Application_Exit 方法中调用 Save 方法,并在 App.xaml.cs 的 Application_Startup 方法中调用 Load 方法,可让应用程序在会话之间保持状态。
添加到队列中的条目已经是 DataItem 类型的。这是一个代表数据的简单类。如果是稍微丰富一些的数据模型,DataItem 可能包含一个简单的对象图。如果是更复杂的情况,DataItem 则是一个基类,其他类派生自这个类。
检索数据
对于在偶尔连接时才可用的应用程序,它必须有办法在本地缓存数据。对此,首先应该考虑的是应用程序所需的工作数据集的大小。在简单的应用程序中,本地缓存可能容得下该应用程序所需的所有数据。例如,使用 RSS 源的简单阅读器应用程序可能只需要缓存源和用户首选项。其他应用程序的工作数据集可能非常大或者很难预测用户需要哪些数据,这样必定会大大增加工作数据集的大小。
对于这个问题,简单应用程序是方便的切入点。应用程序会话中时时需要的数据(例如用户首选项)可以在应用程序启动时下载并保留在内存中。如果用户更改此数据,则可以应用前文讨论的上传策略。但是这种方法是假设应用程序将在启动时连接,可有时情况并非如此。因此隔离存储再次成为解决方法,前文中列举的示例可以满足这种情况,只需再添加一个调用以在合适的时机下载服务器上的数据。请记住,用户可能将应用程序同时安装在工作 PC 和家用 PC 上,因此合适的时机可能是一长段停顿后用户再次返回应用程序时。
另一种情况是显示相对静态的数据(例如新闻)的简单应用程序。对此也可以应用相似的策略:在可能时下载数据,保存在内存中,应用程序关闭时保存在隔离存储中(启动时再从隔离存储重新加载)。当连接可用时,缓存的数据可以失效并刷新。如果连接的时间超过数分钟,应用程序应该定期刷新数据,例如新闻。正如我们在前面所讨论的,UI 应该不知道这些后台工作。
下载新闻时,起点是一个简单的 NewsItem 类:
public class NewsItem
{
public string Headline;
public string Body;
public override stringToString()
{
return Headline;
}
}
这个类在示例中进行了过度简化,而且 ToString 也被覆盖以更容易地在 UI 中绑定。若要存储下载的新闻,需要一个在后台下载新闻的简单 Repository 类,如图 6 所示。
图 6 在 Repository 类中存储下载的新闻
public class Repository
{
private ObservableCollection<NewsItem> news =
new ObservableCollection<NewsItem>();
private DispatcherTimer timer = new DispatcherTimer();
private const int TIMER_INTERVAL = 1;
public Repository()
{
((SampleCode.App)Application.Current).ConnectionStatusChangedEvent +=
new ConnectionStatusChangedHandler(Repository_
ConnectionStatusChangedEvent);
if (((SampleCode.App)Application.Current).IsConnected)
{
RetrieveNews();
StartTimer();
}
}
private void Repository_ConnectionStatusChangedEvent(bool isConnected)
{
if (isConnected)
{
StartTimer();
}
else
{
StopTimer();
}
}
private void StopTimer()
{
this.timer.Stop();
}
private void StartTimer()
{
this.timer.Interval = TimeSpan.FromMinutes(1);
this.timer.Tick += new EventHandler(timer_Tick);
this.timer.Start();
}
voidtimer_Tick(object sender, EventArgs e)
{
if (((SampleCode.App)Application.Current).IsConnected)
{
RetrieveNews();
}
}
private void RetrieveNews()
{
// Get latest news from server
List<NewsItem> list = GetNewsFromServer();
if (list.Count>0)
{
lock (this.news)
{
foreach (NewsItem item in list)
{
this.news.Add(item);
}
}
}
}
private List<NewsItem>GetNewsFromServer()
{
// Simulate retrieval from server
List<NewsItem> list = new List<NewsItem>();
for (int i = 0; i <5; i++)
{
NewsItemnewsItem = new NewsItem()
{ Headline = "Something happened at " +
DateTime.Now.ToLongTimeString(),
Body = "On " + DateTime.Now.ToLongDateString() +
" something happened. We'll know more later." };
list.Add(newsItem);
}
return list;
}
public ObservableCollection<NewsItem> News
{
get
{
return this.news;
}
set
{
this.news = value;
}
}
}
在图 6 中,简单地模拟了新闻的检索。Repository 类订阅 ConnectionStatusChangedEvent 并在连接后使用 DispatcherTimer 按照指定的间隔来检索新闻。DispatcherTimer 与 ObservableCollection 一起使用,以实现简单的数据绑定。DispatcherTimer 集成到 Dispatcher 队列,因此它在 UI 线程上运行。更新 ObservableCollection 的结果是引发事件,使 UI 中的绑定控件自动更新,这对下载新闻来说是理想的解决方法。Silverlight 中提供了 System.Threading.Timer,但它并不在 UI 线程上运行。因此,任何访问 UI 线程上的对象的操作都需要使用 Dispatcher.BeginInvoke 进行调用。
若要使用 Repository,只需要 App.xaml.cs 中的一个属性。假定 Repository 在其构造函数中订阅 ConnectionStatusChangedEvent,对其进行实例化的最佳位置就是在 Application_StartupeventinApp.xaml.cs 中。
当应用程序处于脱机状态时,除用户以外的任何其他人均无法更改用户首选项等数据,但如果同一用户在多种设备上使用应用程序,则其他人完全有可能更改此类数据。新闻故事之类的数据也通常不会更改。这意味着缓存的数据很可能是有效的,再次连接时也不太可能出现同步问题。但是,如果是不稳定的数据,可能需要不同的方法。例如,数据是上次检索的,则需要通知用户以便其做出正确响应。如果应用程序需要根据不稳定的数据做出决策,则需要过期规则,还要通知用户所需的数据不可用,因为应用程序处于脱机状态。
如果数据可以更改,比较何时以及何处进行了更改,在很多情况下都能够解决冲突。例如,乐观的方法会假设最近版本的数据是最有效的,从而解决冲突,但您可能还会决定根据数据的更新位置来解决冲突。了解应用程序拓扑和使用情况是找到正确方法的关键。如果冲突的版本无法(或不应)解决,则应保存有关版本的记录,通知合适的用户让其自行判断。
您还需要考虑在何处放置同步逻辑。最简单的结论是应该将其放在服务器上,以便调节冲突的版本。如果是这样,UploadWorker 则需要一处小小的改动,使其将 DataItem 更新为最新的服务器版本。如前所述,Microsoft Sync Framework 可以解决很多这样的问题,从而让开发人员集中精力处理应用程序。
组合各部分
综合前述所有讨论,可以很容易地在偶尔连接的环境中创建一个简单的 Silverlight 应用程序来提供所有这些功能。图 7 显示了这种应用程序的屏幕快照。
图 7 演示网络状态和队列的示例应用程序
在图 7 的示例中,Silverlight 应用程序运行在浏览器之外。考虑到偶尔连接的应用程序的性质,很多偶尔连接的应用程序都运行在浏览器之外,因为这样用户就很容易从“开始”菜单和桌面进行访问,并且无论网络连接状态如何都可以运行。相反,在浏览器环境中运行的 Silverlight 应用程序则需要网络连接,以便其所在的 Web 服务器可以为 Web 页面和 Silverlight 应用程序提供服务。
图 7 中的简单 UI 尽管肯定无法获得什么设计大奖,却是尝试示例代码的有效手段。除了显示当前的网络状态以外,它还提供了字段以输入数据,以及绑定到 Repository 的 News 属性的列表框。用于创建屏幕的示例 XAML 显示在图 8 中。其后台代码显示在图 9 中。
图 8 示例应用程序 UI 的 XAML
<UserControl x:Class="SampleCode.MainPage"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dataInput="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls.Data.Input"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="12" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="12" />
<RowDefinition Height="64" />
<RowDefinition Height="44" />
<RowDefinition Height="34" />
<RowDefinition Height="34" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<Ellipse Grid.Row="1" Grid.Column="1" Height="30" HorizontalAlignment="Left"
Name="StatusEllipse" Stroke="Black" StrokeThickness="1"
VerticalAlignment="Top" Width="35" />
<Button Grid.Row="4" Grid.Column="1" Content="Send Data" Height="23"
HorizontalAlignment="Left" Name="button1" VerticalAlignment="Top"
Width="75" Click="button1_Click" />
<TextBox Grid.Row="2" Grid.Column="2" Height="23" HorizontalAlignment="Left"
Name="VehicleTextBox" VerticalAlignment="Top" Width="210" />
<TextBox Grid.Row="3" Grid.Column="2" Height="23" HorizontalAlignment="Left"
Name="TextTextBox" VerticalAlignment="Top" Width="210" />
<dataInput:Label Grid.Row="2" Grid.Column="1" Height="28"
HorizontalAlignment="Left" Name="VehicleLabel" VerticalAlignment="Top"
Width="120" Content="Title" />
<dataInput:Label Grid.Row="3" Grid.Column="1" Height="28"
HorizontalAlignment="Left" Name="TextLabel" VerticalAlignment="Top"
Width="120" Content="Detail" />
<ListBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Height="100"
HorizontalAlignment="Left" Name="NewsListBox"
VerticalAlignment="Top" Width="376" />
</Grid>
</UserControl>
图 9 示例应用程序 UI 的后台代码
public delegate void DataSavedHandler(DataItem data);
public partial class MainPage : UserControl
{
private SolidColorBrush STATUS_GREEN = new SolidColorBrush(Colors.Green);
private SolidColorBrush STATUS_RED = new SolidColorBrush(Colors.Red);
public event DataSavedHandlerDataSavedEvent;
public MainPage()
{
InitializeComponent();
((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
ConnectionStatusChangedHandler(MainPage_ConnectionStatusChangedEvent);
IndicateStatus(((NetworkStatus.App)Application.Current).IsConnected);
BindNews();
}
private void MainPage_ConnectionStatusChangedEvent(bool isConnected)
{
IndicateStatus(isConnected);
}
private void IndicateStatus(bool isConnected)
{
if (isConnected)
{
StatusEllipse.Fill = STATUS_GREEN;
}
else
{
StatusEllipse.Fill = STATUS_RED;
}
}
private void BindNews()
{
NewsListBox.ItemsSource =
((SampleCode.App)Application.Current).Repository.News;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
DataItem dataItem = new DataItem
{
Title = this.TitleTextBox.Text,
Detail = this.DetailTextBox.Text
};
DataSavedHandler handler = this.DataSavedEvent;
if (handler != null)
{
handler(dataItem);
}
this.TitleTextBox.Text = string.Empty;
this.DetailTextBox.Text = string.Empty;
}
}
图 9 中的类引发事件以指示数据已保存。一个观察者(在本例中是 App.xaml.cs)订阅了此事件,并将数据推到 ObservableQueue 中。
新型应用程序
Silverlight 支持仅有部分时间连接的应用程序,因而带来一种新型应用程序。这样的应用程序引出了新的注意事项,要求开发人员考虑应用程序在连接时以及断开连接时该如何表现。本文介绍了这些注意事项,并提供了解决问题的策略和示例代码。当然,在偶尔连接的环境中存在多种多样的情况,因此本文中的示例只是抛砖引玉。
Mark Bloodworth 是 Microsoft 开发人员和平台推广团队的架构师,他和同事合作研究各种创新项目。在加入 Microsoft 之前,他是 BBC Worldwide 的首席解决方案架构师,领导一个负责体系结构和系统分析的团队。他的大部分职业生涯都在使用 Microsoft 技术,特别是 Microsoft .NET Framework,还精通一些 Java 技术。他在 remark.wordpress.com 撰写博客。
Dave Brown 已在 Microsoft 工作了九年多的时间,最初在 Microsoft Consulting Services 研究 Internet 相关技术。目前他在英国的开发人员和平台推广团队工作,担任 Microsoft Technology Centre 的架构师。他的研究领域包括客户方案的业务分析、解决方案体系结构的设计以及管理和开发“概念证明”解决方案的代码。他的博客位于 drdave.co.uk/blog。
感谢以下技术专家:Ashish Shetty