<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">博客园_Allen Lee's Magic</title><subtitle type="text">这里没有答案，顶多给你几个值得一试的猜想。</subtitle><id>http://feed.cnblogs.com/blog/u/6996/rss</id><updated>2011-12-10T08:25:57Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><generator>feed.cnblogs.com</generator><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/"/><link rel="self" type="application/atom+xml" href="http://feed.cnblogs.com/blog/u/6996/rss"/><entry><id>http://www.cnblogs.com/allenlooplee/archive/2011/12/10/2283356.html</id><title type="text">WP7有约（八）：在ListPicker控件的选择页面上播放铃声</title><summary type="text">WP7有约（八）：在ListPicker控件的选择页面上播放铃声 Written by Allen Lee 上节课我们在ListPicker控件的选择页面上实现了播放图标的效果，随后sjcxyf同学又发现了新的问题：我在项目里面添加了一个MP3文件，然后我在页面加了一个MediaElement控件，我在Image_Tap事件里面添加了播放音乐的代码，但是表现出来的结果是当选择页面弹出来之后点击播放图标不能播放音乐，我试图尝试在选择模板里面加入MediaElement控件来实现，但是最后结果还是一样。 在Silverlight for Windows Phone里，MediaElemen...</summary><published>2011-12-10T08:00:00Z</published><updated>2011-12-10T08:00:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2011/12/10/2283356.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2011/12/10/2283356.html"/><content type="html">&lt;p&gt;&lt;span style="color: #0070c0; font-size: 20pt;"&gt;&lt;strong&gt;WP7有约（八）：在ListPicker控件的选择页面上播放铃声 &lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color: #0070c0;"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee &lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/12/07/2280008.html"&gt;上节课&lt;/a&gt;我们在ListPicker控件的选择页面上实现了播放图标的效果，随后sjcxyf同学又发现了新的问题：&lt;/p&gt;&lt;p&gt;我在项目里面添加了一个MP3文件，然后我在页面加了一个MediaElement控件，我在Image_Tap事件里面添加了播放音乐的代码，但是表现出来的结果是当选择页面弹出来之后点击播放图标不能播放音乐，我试图尝试在选择模板里面加入MediaElement控件来实现，但是最后结果还是一样。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 在Silverlight for Windows Phone里，MediaElement有一个很特别的限制，你必须把它添加到可视化树，否则它不会播放。假设我们在铃声设置页面上添加一个播放按钮，如图1所示，接着在这个按钮的Click事件处理程序里创建MediaElement对象，然后通过它播放预先添加的铃声（这个铃声文件的Build Action属性的值是Content），如代码1所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201112/20111210155002299.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;图 1 铃声设置页面 &lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;代码 1 通过MediaElement对象播放铃声&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;div style="background-color: #F5F5F5;border: 1px solid #CCCCCC;padding:10px;"&gt;&lt;span style="color: #0000ff;"&gt;private&lt;/span&gt; &lt;span style="color: #0000ff;"&gt;void&lt;/span&gt; Button_Click(&lt;span style="color: #0000ff;"&gt;object&lt;/span&gt; sender, RoutedEventArgs e)&lt;br /&gt;{&lt;br /&gt;    &lt;span style="color: #0000ff;"&gt;var&lt;/span&gt; media = &lt;span style="color: #0000ff;"&gt;new&lt;/span&gt; MediaElement();&lt;br /&gt;    media.Volume = &lt;span style="color: #800080;"&gt;1&lt;/span&gt;;&lt;br /&gt;    media.Source = &lt;span style="color: #0000ff;"&gt;new&lt;/span&gt; Uri(&amp;ldquo;Windows Logon Sound.wav&amp;rdquo;, UriKind.Relative);&lt;br /&gt;}&lt;/div&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 运行这个应用程序，然后单击播放按钮，你会发现什么声音也没有。但如果我们在创建MediaElement对象之后把它添加到LayoutRoot里，如代码2所示，那么单击播放按钮就能听到声音了，因为此时MediaElement对象已在可视化树上了。&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;代码 2 把MediaElement对象添加到LayoutRoot&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;div style="background-color: #F5F5F5;border: 1px solid #CCCCCC;padding:10px;"&gt;&lt;span style="color: #0000ff;"&gt;private&lt;/span&gt; &lt;span style="color: #0000ff;"&gt;void&lt;/span&gt; Button_Click(&lt;span style="color: #0000ff;"&gt;object&lt;/span&gt; sender, RoutedEventArgs e)&lt;br /&gt;{&lt;br /&gt;    &lt;span style="color: #0000ff;"&gt;var&lt;/span&gt; media = &lt;span style="color: #0000ff;"&gt;new&lt;/span&gt; MediaElement();&lt;br /&gt;    LayoutRoot.Children.Add(media);&lt;br /&gt;    media.Volume = &lt;span style="color: #800080;"&gt;1&lt;/span&gt;;&lt;br /&gt;    media.Source = &lt;span style="color: #0000ff;"&gt;new&lt;/span&gt; Uri(&lt;span style="color: #800000;"&gt;"&lt;/span&gt;&lt;span style="color: #800000;"&gt;Windows Logon Sound.wav&lt;/span&gt;&lt;span style="color: #800000;"&gt;"&lt;/span&gt;, UriKind.Relative);&lt;br /&gt;}&lt;/div&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 这正是为什么我们经常看到别人在XAML里创建MediaElement对象，然后在代码隐藏里调用它的Play方法播放音频。不过，如果你试图把代码2照搬到Image控件的Tap事件处理程序里，你会发现还是听不到声音，为什么呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 当我们单击ListPicker控件时，它会打开一个新的选择页面，此时，由于铃声设置页面不再可见，不用渲染，Silverlight for Windows Phone会把它从可视化树上摘除，因此，我们在Image控件的Tap事件处理程序里把MediaElement对象添加到铃声设置页面的LayoutRoot里就失去意义了。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 那么，如果我们在选择页面的项目模板里创建MediaElement对象又会怎样呢？毫无疑问，这会导致同一个页面存在多个MediaElement对象，而&lt;a href="http://msdn.microsoft.com/en-us/library/ff426928(v=VS.96).aspx"&gt;Silverlight for Windows Phone并不支持这种用法&lt;/a&gt;，因此这条路是行不通的。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 从上面的分析不难看出，MediaElement的最大问题在于它对可视化树的依赖，那么，有没有不依赖于可视化树的解决方案？有的，我们可以借用XNA的SoundEffect：&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 1) 添加Microsoft.Xna.Framework库的引用，如图2所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201112/20111210155003988.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;图 2添加Microsoft.Xna.Framework库的引用&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 2) 添加XNA命名空间的引用，如代码3所示。&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;代码 3 XNA命名空间&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;div style="background-color: #F5F5F5;border: 1px solid #CCCCCC;padding:10px;"&gt;&lt;span style="color: #0000ff;"&gt;using&lt;/span&gt; Microsoft.Xna.Framework;&lt;br /&gt;&lt;span style="color: #0000ff;"&gt;using&lt;/span&gt; Microsoft.Xna.Framework.Audio;&lt;/div&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 3) 把Image控件的Tap事件处理程序改为代码4所示的那样。&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;代码 4 通过XNA的SoundEffect播放铃声&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;div style="background-color: #F5F5F5;border: 1px solid #CCCCCC;padding:10px;"&gt;&lt;span style="color: #0000ff;"&gt;private&lt;/span&gt; &lt;span style="color: #0000ff;"&gt;void&lt;/span&gt; Image_Tap(&lt;span style="color: #0000ff;"&gt;object&lt;/span&gt; sender, System.Windows.Input.GestureEventArgs e)&lt;br /&gt;{&lt;br /&gt;    &lt;span style="color: #0000ff;"&gt;using&lt;/span&gt; (&lt;span style="color: #0000ff;"&gt;var&lt;/span&gt; stream = TitleContainer.OpenStream(&lt;span style="color: #800000;"&gt;"&lt;/span&gt;&lt;span style="color: #800000;"&gt;Windows Logon Sound.wav&lt;/span&gt;&lt;span style="color: #800000;"&gt;"&lt;/span&gt;))&lt;br /&gt;    {&lt;br /&gt;        &lt;span style="color: #0000ff;"&gt;var&lt;/span&gt; audio = SoundEffect.FromStream(stream);&lt;br /&gt;        FrameworkDispatcher.Update();&lt;br /&gt;        audio.Play();&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    e.Handled = &lt;span style="color: #0000ff;"&gt;true&lt;/span&gt;;&lt;br /&gt;}&lt;/div&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 代码4所做的事情很简单，它以流的形式打开铃声文件，然后用这个流对象创建SoundEffect对象，并更新XNA的组件状态，最后播放铃声。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 一般情况下，当我们提到Windows Phone应用程序的存储区域时，很多人会第一时间想到独立存储区，不过，Windows Phone应用程序还有另外一个存储区域，这个区域就是应用程序的安装文件夹，和独立存储区不同的是，安装文件夹里面的内容是只读的。当我们在Visual Studio的属性窗口里把内容文件的Build Action属性的值设为Content时，比如前面那个铃声文件，安装程序会把它们部署到安装文件夹里。事实上，Windows Phone SDK 7.1的LINQ to SQL就通过"appdata"和"isostore"在连接字符串里区分这两种区域。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; XNA提供的TitleContainer.OpenStream方法可以用来读取安装文件夹里的内容，如果你的应用是从网上下载铃声的，那么这些铃声将会保存在独立存储区里，这意味着代码4里打开文件流的代码应该换成对应的独立存储访问代码。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;a href="http://files.cnblogs.com/allenlooplee/WindowsPhoneApplication1.zip"&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201112/201112101550038828.png" alt="" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/2283356.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/12/10/2283356.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2011/12/07/2280008.html</id><title type="text">WP7有约（七）：实现铃声设置的播放图标的效果</title><summary type="text">WP7有约（七）：实现铃声设置的播放图标的效果 Written by Allen Lee sjcxyf同学通过站内消息提到这样一个问题：我现在想做一个功能就是当ListPicker弹出全屏幕的时候每一项前面是一个播放图片，后面是音乐名称，然后我点击前面的播放的时候播放当前的音乐，不让他选择这一项的值并返回 要选择后面的字之后才返回 就是像Phone 7真机里面设置铃声那种效果怎么做？ 我们来看一个山寨版的铃声设置，如图1所示，无论用户单击铃声名字还是它左边那个播放图标，都会关闭ListPicker页面。sjcxyf同学想要的效果是单击播放图标将会播放对应的铃声，而单击铃声名字才是确认选...</summary><published>2011-12-07T15:10:00Z</published><updated>2011-12-07T15:10:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2011/12/07/2280008.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2011/12/07/2280008.html"/><content type="html">&lt;p&gt;&lt;span style="color: #0070c0; font-size: 20pt;"&gt;&lt;strong&gt;WP7有约（七）：实现铃声设置的播放图标的效果 &lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color: #0070c0;"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee &lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;a href="http://home.cnblogs.com/u/232531/" target="_blank"&gt;sjcxyf&lt;/a&gt;同学通过站内消息提到这样一个问题：&lt;/p&gt;&lt;p&gt;我现在想做一个功能就是当ListPicker弹出全屏幕的时候每一项前面是一个播放图片，后面是音乐名称，然后我点击前面的播放的时候播放当前的音乐，不让他选择这一项的值并返回 要选择后面的字之后才返回 就是像Phone 7真机里面设置铃声那种效果怎么做？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 我们来看一个山寨版的铃声设置，如图1所示，无论用户单击铃声名字还是它左边那个播放图标，都会关闭ListPicker页面。sjcxyf同学想要的效果是单击播放图标将会播放对应的铃声，而单击铃声名字才是确认选择。问题是，即使你单击铃声下面的空白地方，ListPicker页面也会关闭，为什么？&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201112/201112072248078467.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;图 1 山寨版铃声设置 &lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 查看&lt;a href="http://silverlight.codeplex.com/SourceControl/changeset/view/71382#1510234" target="_blank"&gt;ListPickerPage.xaml.cs文件&lt;/a&gt;，我们将会发现ListPicker页面的ListBox控件添加了Tap事件处理程序，如代码1所示。从中我们不难看出，ListPicker页面表现出来的行为是预期的，即在单选模式下，单击ListBox控件里的选项或者空白地方将会关闭ListPicker页面。于是，sjcxyf同学的需求可以重新描述成在用户单击播放图标时防止ListBox控件的事件处理程序被执行，但是，怎样才能做到？&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;代码 1 ListBox控件的Tap事件处理程序 &lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;div style="background-color: #F5F5F5;border: 1px solid #CCCCCC;padding:10px;"&gt;&lt;span style="color: #0000ff;"&gt;private&lt;/span&gt; &lt;span style="color: #0000ff;"&gt;void&lt;/span&gt; OnPickerTapped(&lt;span style="color: #0000ff;"&gt;object&lt;/span&gt; sender, System.Windows.Input.GestureEventArgs e)&lt;br /&gt;{&lt;br /&gt;    &lt;span style="color: #008000;"&gt;//&lt;/span&gt;&lt;span style="color: #008000;"&gt; We listen to the tap event because SelectionChanged does not fire if the user picks the already selected item.&lt;br /&gt;&lt;br /&gt;    &lt;/span&gt;&lt;span style="color: #008000;"&gt;//&lt;/span&gt;&lt;span style="color: #008000;"&gt; Only close the page in Single Selection mode.&lt;/span&gt;&lt;span style="color: #008000;"&gt;&lt;br /&gt;&lt;/span&gt;    &lt;span style="color: #0000ff;"&gt;if&lt;/span&gt; (SelectionMode == SelectionMode.Single)&lt;br /&gt;    {&lt;br /&gt;        &lt;span style="color: #008000;"&gt;//&lt;/span&gt;&lt;span style="color: #008000;"&gt; Commit the value and close&lt;/span&gt;&lt;span style="color: #008000;"&gt;&lt;br /&gt;&lt;/span&gt;        SelectedItem = Picker.SelectedItem;&lt;br /&gt;        ClosePickerPage();&lt;br /&gt;    }&lt;br /&gt;}&lt;/div&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; Silverlight支持一种叫做&lt;a href="http://msdn.microsoft.com/en-us/library/cc189018(v=VS.95).aspx" target="_blank"&gt;路由事件（routed event）的概念&lt;/a&gt;，这种事件的触发会从子元素沿着对象树向上传递给各个父元素，直至到达根元素为止。并非所有事件都是路由事件，但Tap事件刚好就是路由事件，换句话说，当用户单击播放图标时，ListBox控件作为播放图标的父元素，虽然不是直接包含的父元素，也能感知传递过来的Tap事件。于是，sjcxyf同学的需求可以重新描述成防止播放图标的Tap事件向上传递给父元素，但是，怎样才能做到？&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; Tap事件处理程序的第二个参数有一个Handled属性，这个属性正是用来处理这种需求的。假设播放图标是一个Image控件，如代码2所示，我们可以为它添加一个Tap事件处理程序，并在里面把Handled属性设为true，如代码3所示，这样，Silverlight的事件系统将会停止把Tap事件向上传递给各个父元素。&lt;/p&gt;&lt;p&gt;&lt;span style="color: #4f81bd; font-size: 9pt;"&gt;&lt;strong&gt;代码 2 播放图标的XAML代码 &lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;div style="background-color: #F5F5F5;border: 1px solid #CCCCCC;padding:10px;"&gt;&lt;span style="color: #0000ff;"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color: #800000;"&gt;Image &lt;/span&gt;&lt;span style="color: #ff0000;"&gt;Source&lt;/span&gt;&lt;span style="color: #0000ff;"&gt;="/play.png"&lt;/span&gt;&lt;span style="color: #ff0000;"&gt; Stretch&lt;/span&gt;&lt;span style="color: #0000ff;"&gt;="None"&lt;/span&gt;&lt;span style="color: #ff0000;"&gt; HorizontalAlignment&lt;/span&gt;&lt;span style="color: #0000ff;"&gt;="Left"&lt;/span&gt;&lt;span style="color: #ff0000;"&gt; &lt;br /&gt;       VerticalAlignment&lt;/span&gt;&lt;span style="color: #0000ff;"&gt;="Center"&lt;/span&gt;&lt;span style="color: #ff0000;"&gt; Tap&lt;/span&gt;&lt;span style="color: #0000ff;"&gt;="Image_Tap"&lt;/span&gt;&lt;span style="color: #ff0000;"&gt; Margin&lt;/span&gt;&lt;span style="color: #0000ff;"&gt;="0,0,12,0"&lt;/span&gt;&lt;span style="color: #0000ff;"&gt;/&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;p&gt;&lt;strong style="color: #4f81bd; font-size: 12px; line-height: 18px;"&gt;代码 3 播放图标的Tap事件处理程序&lt;/strong&gt;&lt;/p&gt;&lt;div style="background-color: #F5F5F5;border: 1px solid #CCCCCC;padding:10px;"&gt;&lt;span style="color: #0000ff;"&gt;private&lt;/span&gt; &lt;span style="color: #0000ff;"&gt;void&lt;/span&gt; Image_Tap(&lt;span style="color: #0000ff;"&gt;object&lt;/span&gt; sender, System.Windows.Input.GestureEventArgs e)&lt;br /&gt;{&lt;br /&gt;    &lt;span style="color: #008000;"&gt;//&lt;/span&gt;&lt;span style="color: #008000;"&gt; 添加播放音乐的代码&lt;/span&gt;&lt;span style="color: #008000;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;    e.Handled = &lt;span style="color: #0000ff;"&gt;true&lt;/span&gt;;&lt;br /&gt;}&lt;/div&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 不过，当用户单击播放图标时，当前选中的铃声也会发生改变，这显然不是Windows Phone 7的铃声设置的做法。我们希望单击播放图标时只是播放铃声，但不改变当前选中的铃声，如果此时按Back键返回，铃声设置将会维持原状。这个效果的实现方式和前面的一样，只不过这次换成了MouseLeftButtonUp事件，因此我把它留给你课后实践一下。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;a href="http://files.cnblogs.com/allenlooplee/WindowsPhoneApplication1.zip"&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201112/201112072248071532.png" alt="" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/2280008.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/12/07/2280008.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2011/11/28/2265729.html</id><title type="text">WP7有约（六）：AppBarUtils使用指南</title><summary type="text">AppBarUtils提供了一组Expression Blend行为，可以实现Application Bar上的按钮和菜单项的绑定，在这篇文章里，我们将会具体看看如何使用这个工具包实现相关的功能。</summary><published>2011-11-28T00:41:00Z</published><updated>2011-11-28T00:41:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2011/11/28/2265729.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2011/11/28/2265729.html"/><content type="html">&lt;p&gt;&lt;span style="color:#0070c0; font-size:20pt"&gt;&lt;strong&gt;WP7有约（六）：AppBarUtils使用指南&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee&lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:微软雅黑"&gt;没有你的生活，我开始写小说，好多画面好多灵感，我要把稿费都给你。&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:微软雅黑"&gt;&amp;#8211; 周杰伦, Mine Mine&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;这节课的任务&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 我们知道，Windows Phone的Application Bar并不支持数据绑定，这意味着我们无法像Silverlight的Button控件那样把Application Bar上的按钮或者菜单项直接绑到视图模型的命令属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 为了解决这个问题，我们可以借助一些第三方工具包，比如今天我给大家介绍的AppBarUtils，它提供了一组Expression Blend行为，可以实现Application Bar上的按钮和菜单项的绑定。接下来，我们将会具体看看如何使用这个工具包实现相关的功能。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 首先，假设我们的应用包含了图1-1所示的Application Bar。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835019334.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 11 Application Bar&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;其中，add按钮和clear菜单项分别绑到视图模型的两个命令属性，它们分别负责把数据添加到页面的ListBox控件里和清空ListBox控件里的内容；sync按钮将会执行视图模型的Sync方法；而statistic菜单项将会打开统计结果页面。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 在开始之前，你需要到&lt;a href="http://appbarutils.codeplex.com/"&gt;http://appbarutils.codeplex.com/&lt;/a&gt;下载AppBarUtils的dll，并在项目里引用它。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;按钮和菜单的命令绑定&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 在Expression Blend里单击左边工具栏上的Assets按钮打开Assets窗口，选择Behaviors类别，然后从右边把AppBarItemCommand拖到Objects and Timeline面板的[PhoneApplicationPage]上，如图2-1所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835046430.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 21 添加AppBarItemCommand&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，为什么是拖到[PhoneApplicationPage]上呢？因为Application Bar上的按钮和菜单项并非依赖对象，Expression Blend的行为无法和它们关联，而在Windows Phone的应用程序唯一能够访问Application Bar的地方就是页面，所以我们需要把AppBarItemCommand拖到[PhoneApplicationPage]上。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 重复上述步骤添加一个AppBarItemCommand。现在我们有两个AppBarItemCommand，分别用于add按钮和clear菜单项。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 在Objects and Timeline面板上选中第一个AppBarItemCommand，然后在Properties面板上把Id属性设为add，如图2-2所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835051871.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 22 设置Id属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;因为AppBarItemCommand是根据Text属性的值查找Application Bar上的按钮的，所以Id属性的值必须匹配Text属性的值。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 接着，切换到XAML模式，把AppBarItemCommand的Command属性绑到视图模型的对应的命令属性，如代码2-1所示。&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 21 设置Command属性的绑定&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:blue; font-family:Consolas; font-size:10pt"&gt;&amp;lt;&lt;span style="color:#a31515"&gt;AppBarUtils&lt;span style="color:blue"&gt;:&lt;span style="color:#a31515"&gt;AppBarItemCommand&lt;span style="color:red"&gt; Id&lt;span style="color:blue"&gt;="add"&lt;span style="color:red"&gt; Command&lt;span style="color:blue"&gt;="{&lt;span style="color:#a31515"&gt;Binding&lt;span style="color:red"&gt; AddCommand&lt;span style="color:blue"&gt;}"/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，为什么不直接在Properties面板上设置Command属性的绑定呢？这是因为Expression Blend对Behavior的ICommand类型的属性做了特殊处理，如上面的图2-2所示，这使得Command属性看起来不像普通属性，右边也没有Advanced options按钮，所以无法打开数据绑定对话框。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 至于clear菜单项，我们需要在Properties面板上把Id和Type两个属性的值分别设为clear和MenuItem，如图2-3所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/20111128083507659.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 23 设置Id和Type两个属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，Type属性是用来区分Application Bar上的按钮和菜单项这两种类型的，它的默认值是Button，因此，当你在按钮上使用AppBarItemCommand时，你不需要设置这个属性的值。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 最后是Command属性的绑定，和前面一样，我们需要切换到XAML模式设置绑定表达式，如代码2-2所示。&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 22 设置Command属性的绑定&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:blue; font-family:Consolas; font-size:10pt"&gt;&amp;lt;&lt;span style="color:#a31515"&gt;AppBarUtils&lt;span style="color:blue"&gt;:&lt;span style="color:#a31515"&gt;AppBarItemCommand&lt;span style="color:red"&gt; Type&lt;span style="color:blue"&gt;="MenuItem"&lt;span style="color:red"&gt; Id&lt;span style="color:blue"&gt;="clear"&lt;span style="color:red"&gt; Command&lt;span style="color:blue"&gt;="{&lt;span style="color:#a31515"&gt;Binding&lt;span style="color:red"&gt; ClearCommand&lt;span style="color:blue"&gt;}"/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;使用Expression Blend SDK的行为&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 你知道吗，Expression Blend SDK提供了许多有用的行为，其中一个是CallMethodAction，它可以用来执行指定对象的指定方法，显然非常适合我们的sync按钮，不过，它需要和Trigger一起工作，而Expression Blend SDK并未提供适用于Application Bar的Trigger，怎么办？这个时候就轮到AppBarItemTrigger出场了。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 打开Assets窗口，选择Behaviors类别，然后从右边把CallMethodAction拖到Objects and Timeline面板的[PhoneApplicationPage]上，如图3-1所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835106359.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 31 添加CallMethodAction&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 确保CallMethodAction处于选中状态，在Properties面板上TriggerType右边的New按钮，如图3-2所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/20111128083511753.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 32 单击New按钮&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 在弹出的Select Object对话框里选择AppBarItemTrigger，如图3-3所示，然后单击OK按钮关闭对话框。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835112323.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 33 选择AppBarItemTrigger&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 此时，Trigger的相关属性将会换成AppBarItemTrigger的，把Id属性的值设为sync，如图3-4所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835126194.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 34 AppBarItemTrigger的属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 至于CallMethodAction本身的属性，我们只需把TargetObject属性绑到指定对象，然后把MethodName属性的值设为指定方法的名字就行了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;页面导航行为&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 有了AppBarItemTrigger，我们就可以使用Expression Blend SDK的NavigateToPageAction为Application Bar实现打开页面的操作了，当然，更简单的办法是直接使用AppBarItemNavigation，它的用法和AppBarItemCommand类似，只是Command属性换成了TargetPage属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 如果你希望在URI里包含查询字符串，而参数的值又是绑到视图模型的属性的，那么，你可以试试NavigateWithQueryStringAction。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 打开Assets窗口，选择Behaviors类别，然后从右边把NavigateWithQueryStringAction拖到Objects and Timeline面板的[PhoneApplicationPage]上，如图4-1所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/20111128083515847.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 41 添加NavigateWithQueryStringAction&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 确保NavigateWithQueryStringAction处于选中状态，在Properties面板上Trigger改为AppBarItemTrigger，然后把Id、Type和TargetPage三个属性的值分别改为statistic、MenuItem和/StatisticPage.xaml，如图4-2所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835164303.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 42 设置相关属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 假设我们希望包含的查询字符串是"hitcount=XX&amp;amp;timecount=YY"，其中，XX和YY分别来自视图模型的HitCount和TimeCount两个属性，那么，我们可以通过NavigateWithQueryStringAction的Parameters属性进行设置。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 在Properties面板上单击Parameters属性右边的Edit items in this collection按钮，在弹出的Parameter Collection Editor对话框里单击Add another item按钮添加一个参数，把Field属性的值设为hitcount，把Value属性绑到视图模型的HitCount属性，如图4-3所示。&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835177551.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 43 添加参数&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;用同样的办法添加一个timecount参数，并把它的Value属性绑到视图模型的TimeCount属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 根据上述设置，NavigateWithQueryStringAction将会为我们创建一个这样的URI：/StatisticPage.xaml?hitcount=9&amp;amp;timecount=13（假设用户单击statistic菜单项时，HitCount和TimeCount两个属性的值分别为9和13）。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 由于NavigateWithQueryStringAction是一个TriggerAction，这意味着它的用途并不限于Application Bar，你可以通过Expression Blend SDK的EventTrigger把它用到Silverlight的Button控件（或者其它控件）上。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 最后，如果你需要的只是一个简单的后退，你可以使用GoBackAction。和NavigateWithQueryStringAction一样，你可以通过AppbarItemTrigger把它用到Application Bar上，也可以通过EventTrigger把它用到Silverlight的Button控件（或者其它控件）上。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;绑定启用状态和显示文字&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 有些时候，你可能希望Application Bar上的按钮和菜单项可以根据某些条件自动调整启用状态，比如说，当页面的ListBox控件里有内容时，sync按钮才可用，否则，它应该处于禁用状态。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; AppBarItemTrigger提供了一个IsEnabled依赖属性，当我们把它绑到视图模型的某个bool类型的属性时，前者会监听后者的更改，然后把修改后的值反映到Application Bar上的按钮或者菜单项的IsEnabled属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 如果你使用的是AppBarItemCommand，你可以通过ICommand.CanExecute方法的返回值指定启用/禁用状态，并在状态发生更改的时候触发CanExecuteChanged事件，剩下的事情AppBarItemCommand会帮你处理好的。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 至于Application Bar上的按钮和菜单项的显示文字，你也可能希望实现绑定，这种需求通常出现在多语言支持的应用里，这个时候，你可以把AppBarItemTrigger的Text依赖属性绑到资源对象的某个属性，前者会监听后者的更改，然后把修改后的值反映到Application Bar上的按钮或者菜单项的Text属性。如果你使用的是AppBarItemCommand或者AppBarItemNavigation，你也可以通过它们的Text依赖属性实现同样的效果。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 看到这里，有些同学可能会担心Id和Text两个属性打起架来，放心吧，它们不会的。虽然它们最终都是关联到Application Bar上的按钮或者菜单项的Text属性，但它们发生作用的时间是不同的。Id属性只在行为初始化的时候用来查找Application Bar上的按钮或者菜单项，一旦找到，Id属性就会功成身退了，从此刻开始，Text属性将会派上用场，它会密切关注绑定源，并把更新反映到Application Bar上的按钮或者菜单项的Text属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;下课了&amp;#8230;&amp;#8230;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201111/201111280835199546.png" alt="" /&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/2265729.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/11/28/2265729.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2011/05/27/2059582.html</id><title type="text">WP7有约：一个应用的破蛋过程</title><summary type="text">2011年年度独家巨献，超过半年的写作成果，将近300页的中文教程，全程记录一个WP7应用的开发过程，全新排版，图文并茂，免费下载，不容错失！</summary><published>2011-05-27T02:22:00Z</published><updated>2011-05-27T02:22:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2011/05/27/2059582.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2011/05/27/2059582.html"/><content type="html">&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 不知不觉，《WP7有约》系列文章的发布已经走过半年有多了，它记录了我开发课程表应用时的所思所想，而不仅仅是一个最终结果，喜欢WP7的童鞋，你有木有把相关的东西学到手呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 经过N天的努力，《WP7有约：一个应用的破蛋过程》电子书终于制作完毕了。我重读了这个系列的所有文章，对每个章节进行了更细的划分，使之结构更合理，从而避免连续阅读。你可以在我的博客里阅读这个系列的文章，也可以把电子书下载到你的电脑阅读，无论你最终采用哪种方式来阅读，我的一个建议是：&lt;u&gt;不要仅仅看书，动作做做看，尝试提出新的需求，然后想办法实现它！&lt;/u&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;table width="480"&gt;     &lt;tbody&gt;         &lt;tr&gt;             &lt;td&gt;             &lt;img alt="" height="116" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GT1.png" width="480" /&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/11/17/1879489.html" target="_blank"&gt;             &lt;img alt="创建课程表" height="58" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI11.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html" target="_blank"&gt;             &lt;img alt="创建作业本" height="57" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI12.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/01/11/1933165.html" target="_blank"&gt;             &lt;img alt="创建笔记本" height="58" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI13.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/03/24/1993368.html" target="_blank"&gt;             &lt;img alt="把课程、作业和笔记整合到一块" height="57" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI14.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/05/09/2040785.html" target="_blank"&gt;             &lt;img alt="在主页显示下一节课的信息" height="58" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI15.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://files.cnblogs.com/allenlooplee/WP7%E6%9C%89%E7%BA%A6.7z"&gt;             &lt;img alt="下载电子书" height="58" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI16.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;img alt="" height="116" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GT2.png" width="480" /&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://iridescent.codeplex.com/wikipage?title=Overview" target="_blank"&gt;             &lt;img alt="iridescent" height="57" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI21.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-top: 10px"&gt;             &lt;a href="http://iridescent.codeplex.com/releases/view/65869" target="_blank"&gt;             &lt;img alt="下载源代码" height="58" src="http://images.cnblogs.com/cnblogs_com/allenlooplee/GI22.png" width="480" /&gt;&lt;/a&gt;&lt;/td&gt;         &lt;/tr&gt;     &lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p align="center"&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809509622.png" alt="iridescent" /&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/2059582.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/05/27/2059582.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2011/05/09/2040785.html</id><title type="text">WP7有约（五）：回到主页</title><summary type="text">WP7有约（五）：回到主页Written by Allen LeeIn this farewell, there is no blood, there is no alibi, cause I've drawn regret from the truth of a thousand lies.– Linkin Park, What I've done最重要的问题 还记得当初我们开发这个应用的目的吗...</summary><published>2011-05-09T00:18:00Z</published><updated>2011-05-09T00:18:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2011/05/09/2040785.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2011/05/09/2040785.html"/><content type="html">&lt;p&gt;&lt;span style="color:#0070c0; font-size:20pt"&gt;&lt;strong&gt;WP7有约（五）：回到主页&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee&lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;em&gt;In this farewell, there is no blood, there is no alibi, cause I've drawn regret from the truth of a thousand lies.&lt;br /&gt;&amp;#8211; Linkin Park, What I've done&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;最重要的问题&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 还记得当初我们开发这个应用的目的吗？是为了随时随地可以查看课程信息。但你会闲来无事打开课程表查看课程信息吗？我就不会了！那用户要课程表来干嘛？一般情况下，用户查看课程表是为了回答以下问题：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;下一节课是什么？&lt;/li&gt;&lt;li&gt;今天要上哪些课？&lt;/li&gt;&lt;li&gt;明天要上哪些课？&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;我希望在主页上提供直接获取上述问题的答案的途径，比如说，我们可以在主页上添加下面这个格子（Tile）：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809077734.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;它可以清楚地告诉用户下一节课是什么，几点开始，在哪里上。至于第二、三个问题，我们可以在主页上添加下面两个格子：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080908982.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得注意的是，这两个格子的右上角都有一个箭头记号，这是为了提醒用户单击它们可以获取相关信息。我们不可能在主页上罗列今天和明天的所有课程，这样会导致信息泛滥，同时也会加重用户识别有用信息的负担，因此我们在这里提供一些捷径，以便用户更快到达包含所需信息的地方。此外，用户最常打开的页面应该是当前课程的Course Hub了，在那里用户可以完成当前课程最常用的操作，因此我们可以在主页上再添加一个格子，帮助用户直接到达那个页面：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809099521.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得注意的是，与几点上课相比，用户更加关心几点下课，因此当前课程显示的是下课时间而不是上课时间。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 我们应该把这些格子放在主页上，以便用户随时了解最重要的信息：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809107404.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，怎样创建这样的用户界面呢？其实不难，上面的刷新按钮可以直接使用&lt;a href="http://coding4fun.codeplex.com/"&gt;Coding4Fun Windows Phone Toolkit&lt;/a&gt;的RoundButton控件，而下面的四个格子由于内容结构基本相同，很容易让人想到把它们放到一个集合型的控件里，这里将会选择ItemsControl作为它们的容器。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;创建用户界面&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 首先，在主页上添加一个Panorama项，并把标题设为"课程"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809117894.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;新创建出来的Panorama项默认包含一个Grid，我们可以通过Properties面板上的RowDefinitions属性把它分成上下两行，上面那行用来放置刷新按钮，高度设为自动适应，而下面那行用来放置ItemsControl，高度使用默认设置。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 好了之后，从Assets面板上把RoundButton控件拖到Grid里，默认自动放在Grid的第一行（注意，在使用RoundButton控件之前请先引用Coding4Fun.Phone.Control.dll文件）：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809112321.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;设置ImageSource属性，把图标换成图4里面那个，然后把Content属性的值设为"刷新"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809116748.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;默认情况下，RoundButton控件的文字是位于图标下方的，而且无法直接通过设置某个属性使它的文字放在图标右边，想要达到图4的效果，我们需要修改它的样式。右击RoundButton控件，选择Edit Template\Edit a Copy进入模板编辑状态，此时，你的Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809129191.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;确保StackPanel处于选中状态，然后在Properties面板上把Orientation属性改为Horizontal。接着，选中StackPanel下面的ContentBody，并在Properties面板上把字体大小设为PhoneFontSizeMedium。好了之后退出模板编辑状态，此时，你的RoundButton控件应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809127206.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，为什么要在模板编辑状态里设置字体大小呢？这是因为直接修改RoundButton控件的字体大小不起作用，我认为这是一个bug。我在CodePlex上&lt;a href="http://coding4fun.codeplex.com/workitem/6470"&gt;提了这个问题&lt;/a&gt;，第二天他们就发布了&lt;a href="http://coding4fun.codeplex.com/SourceControl/changeset/changes/64877"&gt;修正的版本&lt;/a&gt;，动作好快！&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 接着，在里面添加一个ItemsControl，在Properties面板上把它的Row属性设为1，并使它充满Grid的第二行。由于ItemsControl默认使用StackPanel来排列它的子元素，这会导致格子从上到下排成一列，要使格子按照图4所示的那样排列，我们需要把StackPanel替换成WrapPanel。右击ItemsControl，选择Edit Additional Templates\Edit Layout of Items (ItemsPanel)\Create Empty：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809136060.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的Create ItemsPanelTemplate Resource对话框里输入模板名字，然后单击OK关闭对话框：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809137946.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，你的Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809146484.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;把里面的StackPanel删掉，然后从Assets面板上把WrapPanel拖到里面：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809145023.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后退出模板编辑状态。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 接下来，我们将会定制每个格子的内容结构。再次右击ItemsControl，选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Create Empty，然后在弹出的Create DateTemplate Resource对话框里输入模板名字，然后单击OK关闭对话框：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809154957.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，你会看到屏幕中间有个空白的Grid：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809159940.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;由于这次我们没有使用设计时数据，数据模板的设计将会比较考究想象力，不过我相信你能跟得上的。确保Grid出于选中状态，把它的Width和Height两个属性都设为190（这个数值是经过多次微调得到的最佳结果），并把它的背景色设为PhoneAccentBrush：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809158827.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后把Grid的Margin设成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809153811.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在Grid里添加相应的控件，使它变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809161760.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，你的Objects and Timeline面板应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809177234.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得提醒的是，在调整控件的大小和位置时，你可以暂时为控件硬编码一些内容，比如上图的"当前课程"和课程信息。好了之后退出模板编辑状态。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;实现内部逻辑&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 现在是时候为那些格子创建对应的ViewModel类了，不过，在开始之前我们需要搞清楚这些格子的工作方式：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;每个格子都有一个标题，标题内容不会改变。&lt;/li&gt;&lt;li&gt;每个格子都有一组信息，用户可以通过单击刷新按钮更新这些信息。&lt;/li&gt;&lt;li&gt;如果用户单击格子时将会执行某个操作，那么格子右上角需要显示箭头图标，否则不显示该图标。&lt;/li&gt;&lt;li&gt;"明天课程"有两个状态：明天有课和明天没课，如果明天有课，则显示课程总数，并显示箭头图标，单击将会打开课程表并显示明天的课程；否则显示"明天没课"，且不显示箭头图标，单击也不执行任何操作。&lt;/li&gt;&lt;li&gt;"今天课程"有三个状态：今天没课、今天有课和已经放学，如果今天没课，则显示"今天没课"，且不显示箭头图标，单击不执行任何操作；如果今天有课，并且还有课程剩余，则显示剩余课程的数目，否则显示"放学了"，这两种状态都会显示箭头图标，单击将会打开课程表并显示今天课程。&lt;/li&gt;&lt;li&gt;"下一节课"也有三个状态：今天没课、今天有课和已经放学，如果今天没课，则显示"今天没课"；如果今天有课，并且还有课程剩余，则显示下一节课，否则显示"放学了"，这个格子不显示箭头图标，单击也不执行任何操作。&lt;/li&gt;&lt;li&gt;"当前课程"有五个状态：今天没课、还没上课、正在上课、课间休息和已经放学，仅当它处于正在上课状态时才显示箭头图标，此时单击将会打开当前课程的Course Hub，处于其它状态时只显示状态名称，且单击不执行任何操作。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;噢，看起来有点复杂哦，不过不用担心，我们一起有步骤地地把它们实现了。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 首先，在ViewModels文件夹里创建一个TileViewModelBase抽象类，并让它继承NotificationObject类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809179153.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这里将会放置被四个格子的ViewModel类共用的代码，那么，哪些代码是共用的呢？根据上面的讨论，每个格子都需要提供一组信息，里面可能包含课程信息或者格子的状态信息，我们可以把这些信息放在一个集合里，并在构造函数里初始化这个集合：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809172500.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;其次，每个格子都有一个标题，但这个标题的内容不会改变，因此我们可以通过自动属性来实现：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809185532.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个属性的初始化需要在每个格子的ViewModel类的构造函数里进行，因为每个格子的标题都不同。至于格子右上角的图标，我们只需提供一个布尔型属性，指示是否显示这个图标就行了，不过，由于这个图标的显示和格子的状态紧密相连，我们需要为这个布尔型属性提供通知机制，以便在格子的状态发生改变时可以通知用户界面做出相应的调整：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809182467.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此外，格子还需要支持单击操作和刷新操作：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809189087.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;由于每个格子在计算应该显示的信息时都要获取今天或者明天的全部课程，我们可以把这部分代码放在基类里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080918482.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;有了这个基类，我们就可以分别为那些格子创建对应的ViewModel类了。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 我们先挑一个简单的来试一下，根据前面的讨论，"明天课程"最简单，因此我们先为它创建ViewModel类。在ViewModels文件夹里创建一个TomorrowCoursesTileViewModel类，并让它继承TileViewModelBase抽象类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809185466.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当用户单击这个格子将会打开课程表并显示明天的课程，为了执行这个操作我们需要知道明天是星期几，因此我在里面添加了一个_day私有字段，用来存放这个信息。接着，我们需要在构造函数里初始化相关的属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809181529.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;其中，打开课程表的代码将会放在（1）里，而初始化格子的代码则会放在（2）里。看到这里，你可能会问，如何在ViewModel类里打开一个页面？一直以来，我们都是在一个页面里通过NavigationService的Navigate方法打开另一个页面，而在ViewModel类里，NavigationService是访问不了的，怎么办？要解决这个问题，我们得先搞清楚Windows Phone的页面导航模型是怎样的，Windows Phone的导航模型是由一个PhoneApplicationFrame和一个或多个PhoneApplicationPage共同组成的，前者提供与导航相关的事件和Navigate方法，而后者则包含应用的内容，它们共享着同一个NavigationService。虽然在ViewModel类里无法访问页面的资源，但是我们可以通过App类获取当前应用的PhoneApplicationFrame对象，继而调用它的Navigate方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809192052.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;事实上，MVVM模式认为ViewModel层不应该依赖于View层的任何东西，更好的做法应该是创建一个INavigationService接口把导航服务隔离开来，然后在ViewModel类里通过依赖注入的方式来获取和访问导航服务，如果你打算为你的ViewModel类创建单元测试，那么这种隔离就更加有必要了。不过，我不想在这里把事情弄得太复杂，因此采用代码9这种简单直接的做法。此外，需要说明的是，仅当格子右上角显示箭头图标时单击格子才会执行相应操作，因此在初始化TileCommand属性的时候我们需要告诉它根据ShowNextIcon属性的值来判断是否允许执行相应的操作。而刷新方面，我们只需要看看明天是否有课，然后根据情况更新相关的信息就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809192335.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，GetChineseDayName方法是一个扩展方法，用来生成与DayOfWeek枚举值对应的中文星期名称，事实上，它是从上节课的CourseHubViewModel类里提取出来的。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 接着，我们来看看"今天课程"，它和"明天课程"没有太大区别，我们只是需要多处理一个状态，以及把显示课程总数改为显示剩余课程的数目。在ViewModels文件夹里创建一个TodayCoursesTileViewModel类，并让它继承TileViewModelBase抽象类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809196970.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;刷新的时候，如果今天的课程数目为0，当然是显示"今天没课"了：&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809205300.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;否则，看看是否所有课程都结束了，如果是就显示"放学了"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080920284.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;否则，显示剩余课程的数目：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080920807.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，这里把剩余课程定义为还没开始的课程，已经开始但还没下课的不包含在里面。此外，Course类的StartTime和EndTime两个属性的值是通过SL for WP Toolkit的TimePicker设置的，由于它只关心时间部分，因此把年、月和日的值都设为1了，而DateTime.Now包含了今天的年、月和日，会对后面的比较造成影响，所以我另外创建了一个thisTime。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 不过，要让打开的课程表正确显示我们想要的课程，还得修改一下课程表的代码。打开CourseTimetablePage.xaml.cs文件，在构造函数里把Loaded事件处理程序的代码改成下面这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080920774.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当用户打开课程表时，默认显示今天的课程，如果查询字符串里面包含了day参数，则显示该参数指定的那天课程。需要说明的是，GetDayOfWeekValue是一个扩展方法，用来生成与中文星期名称对应的DayOfWeek枚举值，本质上它是GetChineseDayName方法的反向实现。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 接着，我们来看看"下一节课"。在ViewModels文件夹里创建一个NextCourseTileViewModel类，并让它继承TileViewModelBase抽象类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809216838.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;根据前面的讨论，这个格子不显示箭头图标，单击也不执行任何操作，因此我们把ShowNextIcon属性的值设为false，把TileCommand属性初始化为"空"命令。看到这里，你可能会问，为什么要这样初始化TileCommand属性呢？因为将来设置数据绑定的时候，我们是在格子的模板里统一设置的，如果某个格子的TileCommand属性为null，那么将来用户单击这个格子时可能会抛出NullReferenceException。刷新的时候，如果今天的课程数目为0就显示"今天没课"了，如果所有课程都结束了就显示"放学了"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809218756.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;否则，我们需要从剩余课程里找出下一节课。如果此时正在上最后一节课呢？前面我们把剩余课程定义为还没开始的课程，那么现在的状态既没有剩余课程又不是已经放学，怎么办？这是一个临界状态，我们可以增加一个状态来描述这种情况，如果现在是最后一节课，那么我们可以显示"最后一节了"，否则显示下一节课的相关信息：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809218723.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 最后，我们来看看"当前课程"，这是四个格子里面最麻烦的，也是状态最多的。在ViewModels文件夹里创建一个CurrentCourseTileViewModel类，并让它继承TileViewModelBase抽象类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809226182.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;刷新的时候，如果今天的课程数目为0就显示"今天没课"了，如果所有课程都没开始就显示"还没上课"，如果所有课程都结束了就显示"放学了"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809222005.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;否则，看看此时是不是正在上课，如果是就显示课程信息，如果不是就显示"课间休息"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809229148.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 看到这里，你可能会问，每次刷新格子都要通过LINQ做很多次遍历，这样会不会降低应用的效率？嗯，是的，目前的做法不是很好，比较好的做法是获取某天的课程，然后对它们进行排序，并缓存到一个集合里，接着从前往后做一次遍历，看看当前时间在哪个位置，从而判断格子处于什么状态。但即使这样，每次刷新还是需要一次遍历，更好的做法应该是每次找到当前时间的位置，就用一个变量做个记号，有点像书签一样，下次刷新时就从这个位置开始往后遍历，这样的话，理想状态下一天就只需做一次遍历了。不过，所有这些都是以一个完整且稳定的课程表为前提的，如果你打算让你的课程表支持临时调课、插课或者和远程服务器进行同步，那么这些优化措施可能会让事情变得糟糕。我不建议太早进行性能优化，因为这样可能会导致设计僵化，我无法告诉你什么时候适合优化性能，你必须根据自己的设计和计划作出决定。但有一点我是可以肯定的，如果性能问题超出了用户的容忍程度，那么无论如何我们都要采取行动了，&lt;span style="text-decoration:underline"&gt;因为用户无法直接看到你的设计有多灵活，却非常清楚他们内心此刻的负面感受。&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 现在，打开MainViewModel.cs文件，它是我们创建这个项目的时候Expression Blend自动为主页创建的ViewModel类，不过里面有很多我们不需要的代码，在继续之前先把它们清理掉，但保留INotifyPropertyChanged接口的实现代码。好了之后，在里面创建下面两个属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809234447.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在构造函数里初始化这两个属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809231590.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;初始化完这两个属性之后别忘了"刷新"一下哦，否则主页上的那些格子不会显示任何东西的。至此，ViewModel类全部创建完毕了，接下来，我们将会把它们关联到对应的View上。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;把ViewModel连接到View上&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 如果你是在Visual Studio里写代码的，那么现在是时候回到Expression Blend了。确保主页上的ItemsControl处于选中状态，在Properties面板上单击ItemsSource属性右边的小正方形，选择Custom Expression，然后在弹出的Custom Expression对话框里输入"{Binding TileViewModels}"。接着，右击ItemsControl，选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Edit Current进入模板编辑状态，根据下表用同样的办法设置TextBlock和ItemsControl的数据绑定：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;&lt;colgroup&gt;&lt;col style="width:144px"&gt;&lt;col style="width:144px"&gt;&lt;col style="width:144px"&gt;&lt;/colgroup&gt;&lt;tbody valign="top"&gt;&lt;tr style="background: #9bbb59"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #b3cc82 1.0pt; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;控件&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #b3cc82 1.0pt; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #b3cc82 1.0pt; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;绑定表达式&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style="background: #e6eed5"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;TextBlock&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;Text&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;{Binding Title}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;ItemsControl&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;ItemsSource&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;{Binding Contents}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;至于格子右上角的箭头图标，我们需要把它的Visibility属性绑到ShowNextIcon属性，不过，它们的类型并不一样，因此我们需要使用转换器。在Properties面板上单击Visibility属性右边的小正方形，选择Data Binding，在弹出的Create Data Binding对话框里选中Data Context选项卡，然后勾选Use a custom path expression，并在旁边输入ShowNextIcon：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080923477.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，单击Value converter右边的省略号按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809231001.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后在弹出的Add Value Converter对话框里选择Coding4Fun.Phone.Controls.Converters下面的BooleanToVisibilityConverter：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809248950.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后单击OK关闭对话框。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 接下来，我们将会把命令对象绑定到相关控件上。首先是格子的单击操作，每个格子最外层是一个Grid，它没有Click事件，因此我选择了MouseLeftButtonUp事件来代替，我希望把它关联到TileViewModelBase的TileCommand属性，怎么关联？还记得&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/03/24/1993368.html"&gt;上节课&lt;/a&gt;介绍的EventToCommand吗？现在又轮到它出场了！从Assets面板上把EventToCommand拖到Grid上，此时，你的Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809255296.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;确保EventToCommand处于选中状态，在Properties面板上把EventName属性设为MouseLeftButtonUp，并把Command属性的绑定表达式设为"{Binding TileCommand}"。好了之后退出模板编辑状态，用同样的办法给RoundButton控件添加EventToCommand，然后把Command属性的绑定表达式设为"{Binding RefreshTilesCommand}"。当我们把EventToCommand拖到一个控件上时，EventName属性会被自动设为目标控件的默认事件，因为Click事件刚好是RoundButton控件的默认事件，所以我们无需对这个属性做任何修改。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 终于可以看效果了，哎呀，我等这个时候等到花儿也谢了，事不宜迟了，按F5吧：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809261326.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图可以看到，今天是星期天，没课，明天是星期一，有三节课，单击"明天课程"将会打开课程表，并显示星期一的课程：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809269591.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;到了星期一早上，如果我们在所有课程开始之前打开应用，主页上的格子就会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809273113.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;过了一会，在第一节课开始之后刷新一下，主页上的格子就会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809281410.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击"当前课程"将会打开Course Hub，并显示当前课程的相关信息，包括课程概况、今天笔记和今天作业：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809295979.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;到了下午放学之后再打开应用，主页上的格子就会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809296785.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;非常好！不过，有一个小小的地方还需要改进一下，目前每个格子里的内容和标题的字体大小是一样的，这意味着它们是同等重要的，但事实上用户更加关注内容而不是标题，因此我们应该增加内容的字体大小，从而更加突出内容的重要性，事实上，通过不同的字体大小区分不同内容的重要性是Metro的设计原则之一。嗯，这个改进的实现就留给你当课后作业吧。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;最近的作业完成得怎样？&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 通过作业本，用户可以清楚地看到每天有什么作业，哪些已经完成，哪些还没完成，但如果用户希望直观地了解每天的作业量有多少，已经完成的占多少，还没完成的又占多少，每天的作业量是否均匀、能否跟得上呢？前面那组是微观层面的问题，而后面那组是宏观层面的问题，要回答宏观层面的问题，我们需要借助统计工具，比如说，我们可以通过下面这个图表回顾过去五天的作业情况：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809305324.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图不难看出，最近作业量增大了，还没完成的作业比重也在增大，是时候找找原因了。不过，查明这个原因不是我们的任务，实现这个功能才是我们的任务。那么，如何实现这个功能？&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 实现这个功能需要从两个方面入手，第一，实现一个算法计算统计结果，第二，使用一个控件显示统计结果。我们先来看看第一个方面，这个算法的基本思路是先找出过去五天的作业，然后把它们分成已完成和未完成两组，接着分别对每组进行统计，计算每天的作业有多少。换句话说，我们的统计结果将会产生两组二维数据，我们可以通过两个集合来保存它们：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809303654.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这两组数据的计算过程基本上是一样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809319128.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;唯一的区别在于一个要筛选出已完成的作业，另一个要筛选出未完成的作业，这个"变数"可以通过方法的参数隔离开来。根据图31，我们可以创建一个ComputeStatistic方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809319652.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们可以通过方法的参数指定计算哪组数据，此外，在执行任何具体计算代码之前，我们需要知道过去五天的日期是什么。看到这里，你可能会问，每次调用这个方法都要创建这样一个集合会不会很低效？是的，你可以把它保存在一个属性里，然后在构造函数里对它进行初始化，这样每次调用这个方法时只需访问那个属性就行了，不过，如果你决定这样做，你需要额外考虑一个问题：当用户在使用的过程中时间跨越了零点，应用需要重新根据新的"今天"重新创建这个集合，诚然，这种情况发生的几率不大，但若放任不管，它就会成为一个潜在的bug。接下来，执行图31所示的五个计算步骤：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809311570.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，如果现有的作业不足五天呢，比如说一开始没有任何作业？这种情况下，我们应该为没有作业的那些天按顺序"补零"，以便横坐标可以正常显示过去五天：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809322094.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;有了ComputeStatistic方法，我们就可以实际计算代码24那两个属性的值了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080932665.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，什么时候调用这个方法呢？我们知道，作业的统计数据不会因为时间的流逝而自动改变，因为作业的新建和编辑都需要用户手动完成，而主页上面也没有提供任何直接操作的途径，所以我们无需在主页上为这个统计图表提供一个单独的刷新按钮，只需在页面加载的时候刷新一下就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809329553.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接下来，我们将会把统计结果以统计图表的方式呈现出来。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 这里选择&lt;a href="http://www.mindscapehq.com/products/phone-elements/controls/wp7-stacked-bar-chart"&gt;Mindscape Phone Elements的Stacked Bar Chart&lt;/a&gt;，当然，你也可以使用其它熟悉的图标控件。&lt;a href="http://assets.mindscape.co.nz/Downloads/PhoneElementsTrial.msi"&gt;下载&lt;/a&gt;并添加Mindscape Phone Elements的类库的引用，然后在主页上添加一个Panorama项，并把标题设为"作业"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809333075.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，从Assets面板上把Chart控件拖到Grid里，并使之充满整个Grid：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809339977.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们的统计图表包含两组统计数据，因此我们需要在Chart控件里添加两个StackedBarSeries控件，此时，你的Objects and Timeline面板应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809348242.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这两个StackedBarSeries控件将会分别绑到FinishedStatistic和UnfinishedStatistic两个属性。怎么绑呢？选中第一个StackedBarSeries控件，在Properties面板上按照下表设置相关属性的绑定表达式/值：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;&lt;colgroup&gt;&lt;col style="width:144px"&gt;&lt;col style="width:192px"&gt;&lt;/colgroup&gt;&lt;tbody valign="top"&gt;&lt;tr style="background: #9bbb59"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #b3cc82 1.0pt; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #b3cc82 1.0pt; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;绑定表达式&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style="background: #e6eed5"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;ItemsSource&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;{Binding FinishedStatistic}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;XBinding&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;{Binding Key}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style="background: #e6eed5"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;YBinding&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;{Binding Value}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #b3cc82 1.0pt; border-bottom:  solid #b3cc82 1.0pt; border-right:  none"&gt;&lt;p&gt;SeriesBrush&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  none; border-bottom:  solid #b3cc82 1.0pt; border-right:  solid #b3cc82 1.0pt"&gt;&lt;p&gt;#FFDB843D&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要注意的是，ItemsSource属性的类型是IList，这正是为什么我把代码24的两个属性的类型声明为IList，当然，你也可以把它们声明为List&amp;lt;KeyValuePair&amp;lt;string, int&amp;gt;&amp;gt;。至于第二个StackedBarSeries控件，你只需把ItemsSource的绑定表达式设为"{Binding UnfinishedStatistic}"，并为SeriesBrush属性换一种颜色就行了。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 现在，按F5看看效果：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809354828.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;进入作业本添加一些作业，然后回到主页看看：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809352777.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;如果作业本里保存了过去五天的作业数据，那么统计图表将会显示成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080936552.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;目前，我们是硬性规定显示过去五天的统计结果，这个设定显然无法满足所有用户，比如说，有些用户希望查看过去七天甚至过去十天的统计结果，这个时候可以考虑在应用设置里给出选项。如果你决定给予用户这个选择权，那么你就需要考虑在保证统计图表清晰可辨的情况下最多能够显示多少天的统计结果，你需要给定一个选择上限，同时向用户解释这样做的原因。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;哪些内容是重点？&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 教育的一个重要方面是不断强调，这主要表现为重要的内容经常重复出现。当我们的笔记本记满了笔记，并且通过标签做好分类，我们很自然就会问，哪些标签的出现频率最高，因为这些拥有这些标签的笔记通常都是课堂重点/考试要点。这个时候又轮到统计工具出场了，我们可以通过饼图列出七个出现频率最高的标签，剩下的统一归入"其它"类别，像这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809376026.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;为什么是七个呢？这和大脑的短期记忆容量有关，长期研究表明，这个容量是7&amp;#177;2。事实上，当我们在设计Windows Phone应用的用户界面时，我们应该把短期记忆容量考虑进去，不要在上面放置太多东西，以免用户产生心理饱和。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 事不宜迟了，我们动手吧！创建一个Windows Phone Page，并把它命名为TagStatisticPage.xaml：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809384988.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后把应用程序标题和页面标题分别改为"笔记本"和"标签统计"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809398303.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，从Assets面板上把PieChart控件拖到页面的ContentPanel里，并使之充满整个ContentPanel：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809393253.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们的饼图包含了一组统计数据，因此我们需要在PieChart控件里添加一个PieSeries控件，此时，你的Objects and Timeline面板应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080940712.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 42&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接下来，我们需要为这个页面创建一个ViewModel类。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 在ViewModels文件夹里创建一个TagStatisticViewModel类，并在里面创建一个TagStatistic属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809409283.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们将会在构造函数里初始化这个属性，由于标签统计是针对每个课程而不是所有课程来做的，因此我们需要通过构造函数的参数传递课程名称：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/20110509080940679.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个属性的计算过程并不复杂：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809414168.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 43&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;前五个计算步骤可以通过LINQ轻松实现：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809416087.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;至于最后一步，我们需要分情况处理，如果统计结果包含的分组在七个或者以内，那么我们只需直接显示统计结果；如果超过七个，那么我们需要把剩余的分组聚合到"其它"分类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809414102.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;创建好ViewModel类之后，我们就可以把它关联到TagStatisticPage页了。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 打开TagStatisticPage.xaml.cs文件，在里面创建一个OnNavigatedTo方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809422989.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这里假设查询字符串里面包含了一个名为coursename的参数。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 最后，我们需要在NoteBookPage页里添加一个Application Bar菜单项，用于打开TagStatisticPage页：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809427973.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 44&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;并在它的Click事件处理程序里添加相关代码：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809428463.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得注意的是，如果课程表没有任何课程，那么我们就不应该打开TagStatisticPage页了，不过，我们需要通过消息框向用户说明一下。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 现在，按F5，然后进入笔记本，打开TagStatisticPage页：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809435017.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 45&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;嗯，很好！不过，有几个地方需要调整一下：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;我希望统计图表上方能够显示课程名称；&lt;/li&gt;&lt;li&gt;目前统计图表是从三点位置顺时针展开的，我希望改成从零点位置顺时针展开；&lt;/li&gt;&lt;li&gt;我希望统计图表的配色方案和图38的一样。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;第一个问题很容易解决，我们只需在TagStatisticViewModel类里创建一个Title属性，并在构造函数里把它初始化为课程名称，然后把它绑到PieChart控件的Title属性就行了。第二个问题也很好解决，我们只需把PieSeries控件的StartAngle属性的值改为270就行了。至于第三个问题，我们需要设置PieSeries控件的Brushes属性，按顺序添加相应颜色的SolidColorBrush对象，你可以直接写XAML，也可以通过Blend的Brush Collection Editor来设置：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809443281.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 46&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我采用的是第二种做法，因为它允许我通过取色器直接从图38上获取颜色，当然，如果你有自己的配色方案，并且知道那些颜色的HTML代码，那么直接写XAML也很方便。改好之后重新运行，然后打开TagStatisticPage页：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809457327.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 47&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;对比图45，图47给人一种相容有序的感觉，这在很大程度上得益于配色方案的选择，这些颜色都是基于同一色相的，因此能够给人一种相容的感觉，同时，这些颜色根据明度（明亮程度）从小到大顺时针排列，因此给人一种有序的感觉，此外，不同明度能使颜色产生不同的"重量"，明度较低的颜色给人感觉较重，因此用在出现频率较高的标签上，而明度较高的颜色给人感觉较轻，因此用在出现频率较低的标签上，这样用户可以直观地感知内容的重要程度。&lt;span style="text-decoration:underline"&gt;一个好的应用除了能够帮助用户解决相关的问题，还应该照顾到用户的无意识需求&lt;/span&gt;，这不但对于用户来说是一件好事，对于开发者来说也是一件好事，最自然的操作是凭直觉的操作，最自然的选择是符合直觉的选择，这是一种无需经过思考的条件反射，当用户的无意识需求被照顾到了，他们也会不经意地选择你的应用，他们甚至很难解释清楚为什么就要选择你的应用。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;你一直想要的东西&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 从《WP7有约》系列的第一篇文章发布至今，我收到了无数封索要代码的邮件，但都一一回绝了。如果你在学习这些文章的过程中遇到任何问题，我都乐意为你解答，你可以在文章下面留下评论，我会尽快回复你，这样做的好处是其他人遇到相同/类似问题的同学也可以看到这些交流内容，当然，我也欢迎你通过其它方式和我交流。如果你的问题确实很难描述清楚，你也可以把你的代码发给我，我帮你看看到底哪里出了问题，我之所以愿意花费这个时间是因为你认真对待了这些文章的学习。为什么我会这么肯定呢？因为这个系列的所有代码都是以图片形式呈现的，这导致你无法简单地把它们复制粘贴到你的工程里，你必须亲自输入一遍，这是故意的，因为仅仅看一遍文章虽然可以让你知道那么一回事儿，但这些知识只会暂存在大脑的短期记忆里，过一段时间就会变得模糊，而亲自动手做一遍练习却可以帮助你把知识转存到大脑的长期记忆里，同时，动手的过程中会遇到各种各样的问题，这些问题可以激发你的思考，这对巩固你学到的知识是很有帮助的，因此，当你拿着亲自输入的代码来问我问题时，我也会认真地回答你。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 当然，前来索要代码的同学并不都是为了学习这些文章，每个人都有自己的想法和目的，这我能理解，因此，在这个最后的时刻，没错，本文是这个系列的最终回了，我把代码开源出来，你可以到&lt;a href="http://iridescent.codeplex.com/"&gt;iridescent.codeplex.com&lt;/a&gt;下载，这样，你就可以：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;编译代码，然后安装到你的Windows Phone上使用，当然，前提是你的手机已经成功越狱；&lt;/li&gt;&lt;li&gt;提取里面的某些代码，然后用到你自己的项目里；&lt;/li&gt;&lt;li&gt;根据你的需求对应用进行定制，比如去掉一些你不需要的功能，增加/增强一些你所需要的功能；&lt;/li&gt;&lt;li&gt;甚至，你可以把你的定制版本作为免费/付费应用发布到Marketplace上。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;事实上，我也鼓励你根据自己的需求对这个应用进行定制，在这个过程里，你已经学到的知识将会得到巩固，因为你需要充分理解这些知识才能找到恰当的切入点，同时，为了实现新的需求，你会去学习新的知识，并在实践的过程中把它们整合到现有的知识体系里，此外，你的定制版本还可以帮助你以及有类似需求的潜在用户，实乃一箭三雕！&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 除了代码，另一个被提及最多的要求就是提供这些文章的打包下载。会有的，我会把这些文章整合起来，添加目录，适当排版，然后生成PDF提供下载，不过，这需要一些时间，敬请耐心等待。&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 最后，终于到了真正的最后，感谢各位一直以来的支持，特别感谢PRO为本系列文章精心打造了一个WP7手机外壳，以及他在图片合成方面为本系列文章提供的帮助！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;下课了&amp;#8230;&amp;#8230;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201105/201105090809509622.png" alt="" /&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/2040785.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/05/09/2040785.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2011/03/24/1993368.html</id><title type="text">WP7有约（四）：课程全景</title><summary type="text">WP7有约（四）：课程全景Written by Allen LeeDo I trust some and get fooled by phoniness, or do I trust nobody and live in loneliness?– Linkin Park, By Myself化零为整 前面三节课我们分别实现了课程表、作业本和笔记本三个主要功能，然而，它们的内容分散在三个不同的页面...</summary><published>2011-03-24T00:15:00Z</published><updated>2011-03-24T00:15:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2011/03/24/1993368.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2011/03/24/1993368.html"/><content type="html">&lt;p&gt;&lt;span style="color:#0070c0; font-size:20pt"&gt;&lt;strong&gt;WP7有约（四）：课程全景&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee&lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Do I trust some and get fooled by phoniness, or do I trust nobody and live in loneliness?&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;&amp;#8211; Linkin Park, By Myself&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;化零为整&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;前面三节课我们分别实现了&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/11/17/1879489.html"&gt;课程表&lt;/a&gt;、&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html"&gt;作业本&lt;/a&gt;和&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/01/11/1933165.html"&gt;笔记本&lt;/a&gt;三个主要功能，然而，它们的内容分散在三个不同的页面，试想一下，如果我想查看某门课下一次什么时候上课、今天有哪些作业要做以及今天记了哪些笔记，我就不得不在多个不同的页面之间来回切换了，这显然降低了应用程序的可用性（usability）！因此，这节课的任务是化零为整，把相关内容整合起来。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;在WP7里，内容的整合一般是通过"Hub"来实现的，比如说，People Hub、Pictures Hub、Music + Video Hub、Office Hub、Games Hub以及Marketplace Hub等都是典型的代表。那么，如何创建这样的页面？非常简单！右击Projects面板里的项目节点，选择Add New Item：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803148118.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的New Item对话框里选择Windows Phone Panorama Page，输入页面的名字，然后按OK：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803154954.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Expression Blend会为你创建一个Panorama页，里面包含了两个Panorama项。在Properties面板上把Panorama控件的Title属性设为"组织行为学"，接着，添加一个Panorama项，然后，把页面上的三个Panorama项的Header属性分别设为"课程概况"、"今天笔记"和"今天作业"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803161300.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这三个Panorama项分别用于显示当前课程的基本信息、今天记录的笔记以及今天要做的作业。接下来，我们将会详细探讨每个Panorama项的设计。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先是"课程概况"，我希望它能告诉我每周星期几有课以及下节课的相关信息：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803161757.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，这是怎么弄的？你可以在Panorama项里放置两个TextBlock，也可以放置一个ListBox，你的决定将会影响到后面的实现，这里没有谁对谁错，你要做的只是权衡利弊，做出决定，然后承担责任。这里选择了后一种做法，主要是考虑到将来添加其它信息时可以更加方便。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接着是"今天笔记"，要显示今天的笔记并不难，也就是一个ListBox的功夫：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803182672.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然而，仅仅这样就够了吗？我们知道，新建和查看是最常用的两项操作，可以满足用户绝大多数时候的需求，我们应该尽可能让用户最快地接触到常用的功能，这意味着我们可以考虑把新建操作集成到这里，但完整的新建操作需要打开NewOrEditNotePage页才能完成啊，而CourseHubPage页的设计原则是尽可能让涉及到的操作"就地完成"，并且用户可以马上看到结果，怎么处理？要回答这个问题，我们得先搞清楚用户在什么情况下会通过CourseHubPage页来新建笔记。我们知道，用户在新建笔记的时候需要提供笔记内容和笔记标签，然而，只有笔记内容是必须提供的，笔记标签的提供可以延后，如果用户要在极短时间内新建笔记，比如说在课堂上，那么暂时忽略笔记标签，只输入笔记内容显然会极大地加速整个操作过程，换句话说，用户只需一个TextBox和一个Button就可以完成新建操作：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803204393.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要注意的是，TextBox的TextWrapping属性的值默认是Wrap，这是为了让TextBox能够根据内容的长度自动调整自身的高度，然而，这个好处在这里会导致下面的ListBox被挤压，从而使得笔记内容的显示空间减少，这显然不是我们希望看到的，因此我们需要把TextBox的TextWrapping属性的值设为NoWrap。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;最后是"今天作业"，出于相同的理由，我也为它配备了新建功能：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080322640.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然而，仅仅这样就够了吗？当然不够，首先，用户无从知晓哪些作业还没完成，其次，当用户完成一项作业时，很自然地想把它标记为已完成，然而，就目前的设计而言，用户得先退回主菜单，然后进入作业本找到并编辑这项作业，这个繁琐的过程无疑降低了应用程序的可用性。有没有办法可以简化这个过程？我们知道，作业的完成状态是通过IsCompleted属性来标识的，这个属性的类型是bool，一般而言，如果我们要在用户界面上表达这个类型的数据，我们会选择CheckBox，因此，我们不妨为每项作业配备一个CheckBox：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803235458.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，用户既可以直观地了解作业的完成状态，又可以轻易地更改作业的完成状态，而我们唯一要做的只是创建一个双向数据绑定！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;连接前端和后端&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;有了用户界面，接下来就是为它创建对应的ViewModel类了。首先，在ViewModel文件夹里创建一个CourseHubViewModel类，并让它继承NotificationObject类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803241521.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，创建以下三个属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803249536.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;它们将会分别绑到课程概况、今天笔记和今天作业上的ListBox。那么，我们应该如何初始化它们呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;Assignments属性的初始化最简单，因为Assignment类里有个StartDate属性，我们可以根据这个属性在CouseHubViewModel类的构造函数里筛选出今天的作业：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803242884.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;但是，Note类没有类似的属性啊，怎么办？创建一个吧：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803245359.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样我们就可以像筛选作业那样筛选笔记了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803255882.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最麻烦的是Overview属性，它需要我们重新组织/聚合课程的信息，比如图4的第一条信息&amp;#8212;&amp;#8212;"逢星期一、星期二、星期五有课"，其中"逢XXX有课"是固定部分，"星期一、星期二、星期五"是可变部分，这部分信息保存在Course对象的Day属性里，我们可以通过LINQ查询课程表里特定课程的所有Course对象，然后提取它们的Day属性，并按照我们期望的格式聚合起来：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080325309.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;至于第二条信息，我们得先找到下节课的Course对象，怎么找？想想看，如果我们手头上有一份课程表，我们会怎么找呢？我们会先看看明天有没有这节课，如果有，那就是它了，如果没有，看看后天有没有，如此类推，下星期的今天为止。我们可以模拟这个过程查找下节课的Course对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080325276.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，某个课程（Course对象）星期几有课是以字符串的形式表示的，而某天（DateTime对象）星期几却是以DayOfWeek枚举的形式表示的，因此我们需要一个GetChineseDayName方法把DayOfWeek枚举转换成对应的中文字符串：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080325243.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，请思考一下，GetChineseDayName方法有没有可能抛出ArgumentException异常？如果有，什么情况下会抛出这个异常？如果没有，为什么？好了，找到下节课之后，我们就可以着手相关信息的聚合了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080326767.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得提醒的是，这里的做法是不具备多语言扩展的，不过，就目前而言，这已足够了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来，我们将会实现"今天笔记"的新建操作，从用户界面上看，这个功能是由一个TextBox和一个Button组成的，前者只需配备一个对应的字符串属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803269338.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;至于后者，根据上节课的经验，我们需要为它创建一个NewNoteCommand属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803267146.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后在构造函数里把它初始化为一个DelegateCommand对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803263765.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;DelegateCommand类的构造函数接受两个参数，第一个是执行这个命令时将会调用的代码，第二个是判断这个命令能否调用的代码。当用户单击按钮时，我们需要根据当前课程、笔记内容以及今天日期等信息创建一个Note对象，然后保存这个Note对象。现在的问题是，如何通知页面的ListBox更新？办法其实有很多，比如说，我们可以学&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html"&gt;第二节课&lt;/a&gt;那样，手动监听CollectionChanged事件，也可以学&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/01/11/1933165.html"&gt;上节课&lt;/a&gt;那样，通过CollectionViewSource间接监听CollectionChanged事件，不过，这次我打算采用更加直接的办法，在保存Note对象之后手动把它添加到Notes属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080326701.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，这里不是通过Add方法把Note对象添加到Notes属性的末尾，而是通过Insert方法把Note对象添加到Notes属性的首位，为什么这样呢？想想看，当用户单击按钮时，其视线将会落在按钮及其附近，如果新建的笔记显示在ListBox的首位，那就正好落在用户的视线范围里面，这样用户就无需挪动视线寻找并确认新建的笔记了。完成这些操作之后，我们需要把TextBox清空，为下次输入做好准备，因为TextBox是绑到NoteContent属性的，所以我们只需把NoteContent属性的值重设为空字符串就行了。至于DelegateCommand类的构造函数的第二个参数，即判断这个命令能否调用的代码，也很简单，只有当TextBox里有内容，那么单击按钮才会执行这个命令：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803278716.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;"今天作业"的新建操作的实现方式和这里的一样，我打算把它留给你当今天作业，值得提醒的是，在创建Assignment对象时，你需要把相关属性初始化为恰当的值，这可以参考&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html"&gt;第二节课&lt;/a&gt;的做法。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;最后，页面的课程名称也需要一个对应的属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803278159.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，为什么这里直接使用自动属性，而不像代码10的NoteContent属性那样在set访问器里调用RaisePropertyChanged方法？这是因为，在用户访问CourseHubPage页期间，当前课程是不变的，即CourseName属性的值不会发生改变，因此简单的自动属性已经足够了。CourseName属性的初始化也非常简单，只需把传给页面的课程名称赋给它就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803279555.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;创建好ViewModel类之后，我们就可以着手处理它和CourseHubPage页之间的关联了。首先是设置数据绑定，需要设置的控件以及对应的绑定表达式如下表所示：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;&lt;colgroup&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;col style="width:336px"&gt;&lt;/colgroup&gt;&lt;tbody valign="top"&gt;&lt;tr style="background: #9bbb59"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;描述&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;类型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;绑定表达式&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;页面标题&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;TextBlock&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Text&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding CourseName}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;课程概况&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;ListBox&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;ItemsSource&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding Overview}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;今日笔记&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;ListBox&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;ItemsSource&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding Notes}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;笔记内容&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;TextBox&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Text&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding NoteContent, Mode=TwoWay}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;今日作业&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;ListBox&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;ItemsSource&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding Assignments}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;作业内容&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;TextBox&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Text&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding AssignmentContent, Mode=TwoWay}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，还有两个按钮呢？上节课我们曾经说过，SL for WP的Button控件没有Command属性，不能直接绑定命令对象，我们需要通过Behavior间接实现Button控件和命令对象之间的绑定，但Prism没有为Button控件提供现成的Behavior，怎么办？我们可以仿照Prism的&lt;a href="http://compositewpf.codeplex.com/SourceControl/changeset/view/52595"&gt;ApplicationBarButtonCommand&lt;/a&gt;创建一个适用于Button控件的ButtonCommand，也可以直接使用&lt;a href="http://www.microsoft.com/downloads/en/details.aspx?FamilyID=15718049-f993-4d54-ba7b-72e0e82c16cd&amp;amp;displaylang=en"&gt;Windows Phone 7 Developer Guide &amp;#8211; Code Samples&lt;/a&gt;里面提供的ButtonCommand，还可以使用&lt;a href="http://mvvmlight.codeplex.com/"&gt;MVVM Light Toolkit&lt;/a&gt;的EventToCommand。毫无疑问，第二种方案是最简单的，而第三种方案则是最强大的，它不但可以把任意事件映射到任意命令对象，最新版本还支持把事件处理程序的参数传给命令对象。下面将会示范如何通过EventToCommand给Button控件绑定命令对象。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，引用GalaSoft.MvvmLight.Extras.WP7.dll类库，接着，打开Assets面板，选择Behaviors，然后把EventToCommand拖到Objects and Timeline面板的Button上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803297372.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803302813.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;确保EventToCommand处于选中状态，在Properties面板上把Command属性的值设为"{Binding NewNoteCommand}"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803302780.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，不用设置事件吗？如果你查看Properties面板上的EventName属性，你会发现它已经设为Click了，因为Button控件的默认事件就是Click。是不是很简单呢，剩下那个Button控件也是这样处理哦。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，万事俱备只欠&amp;#8230;&amp;#8230;嗯，创建一个CourseHubViewModel对象，并把它赋给CourseHubPage页的DataContext属性，但是，我们从哪获取课程名称呢？我们知道，查询字符串是页面之间传递信息的一个主要途径，我们可以假设从某个页面来到CourseHubPage页时，查询字符串里包含了一个名为"coursename"的参数，然后在OnNavigatedTo方法里通过NavigationContext.QueryString来访问：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803311667.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，用户如何打开这个页面？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;打开CourseHubPage页&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;什么地方最适合用来打开CourseHubPage页呢？我们知道，这个页面不能直接打开，因为在打开之前我们必须提供一个课程名称，因此，仿效前三节课在主菜单里通过菜单项打开页面的做法是行不通的。如果换了传统的桌面应用程序，我们可能会使用ComboBox和Button这个组合，在ComboBox里列出所有课程名称让用户选择，选好之后单击按钮打开CourseHubPage页，可是，这里是手机应用啊，还有没有更好的方式呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;毫无疑问，CourseHubPage页需要一个课程上下文，哪里可以提供这个上下文呢？课程表！那么，如何设计打开CourseHubPage页的操作呢？有两种可能的方案，第一种是在课程表的Application Bar上添加一个"打开"按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803329060.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，用户可以通过单击选中某个课程，然后单击"打开"按钮打开CourseHubPage页。第二种是把编辑和删除两个操作放在课程的上下文菜单里，把保存操作改成Application Bar的按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803328960.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，用户可以通过单击课程打开CourseHubPage页，通过长按课程打开上下文菜单，然后执行编辑或删除操作。这里选择第二种方案，这样课程表的页面设计和操作体验可以与作业本、笔记本的保持一致。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;打开CourseTimetablePage.xaml.cs文件，把ApplicationBarCommitMenuItem_Click方法重命名为ApplicationBarCommitIconButton_Click，然后把"保存"按钮的Click事件处理程序设为ApplicationBarCommitIconButton_Click：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803333944.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，打开CourseTimetablePage.xaml文件，在courseCollectionItemTemplate数据模板里添加上下文菜单，并把两个菜单项的Click事件处理程序分别设为EditMenuItem_Click和DeleteMenuItem_Click：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803337.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，这次是否也学前面那样，把ApplicationBarEditIconButton_Click和ApplicationBarDeleteIconButton_Click两个方法分别重命名为EditMenuItem_Click和DeleteMenuItem_Click呢？不行哦，因为前后两种操作方式背后的实现原理是不一样的，之前的操作方式是单击选中课程然后单击Application Bar上的按钮执行相应操作，因此我们可以通过ICollectionView.CurrentItem来获取当前选中的课程，而现在的操作方式是长按课程打开上下文菜单然后单击菜单项执行相应操作，执行操作的时候目标课程不会变成选中状态，因此通过ICollectionView.CurrentItem来获取目标课程是行不通的，怎么办？上节课我们曾经说过，上下文菜单能从列表项那里继承DataContext属性的值，而这个值正是目标课程，因此我们可以通过菜单项的DataContext属性获取目标课程的相关信息：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803337466.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，编辑、删除和保存三个操作就改造完毕了，那么，打开CourseHubPage页的操作呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;一般情况下，我们通过Click事件处理程序来实现单击操作的，然而，正如代码24所示的那样，列表项的最外层容器是StackPanel，它没有Click事件，怎么办？有三种可能的方案，第一种是通过StackPanel的MouseLeftButtonUp事件模拟单击操作，这种方案最简单，但不够精确，因为Click事件本身是由MouseLeftButtonDown和MouseLeftButtonUp两个事件组成的，如果你想获得更加精确的效果，可以使用&lt;a href="http://www.microsoft.com/downloads/en/details.aspx?FamilyID=15718049-f993-4d54-ba7b-72e0e82c16cd&amp;amp;displaylang=en"&gt;Windows Phone 7 Developer Guide &amp;#8211; Code Samples&lt;/a&gt;里面提供的FrameworkElementClickCommand，它不但正确处理了MouseLeftButtonDown和MouseLeftButtonUp两个事件，还提供了命令对象的绑定，最后一种是使用SL for WP Toolkit的GestureService/GestureListener组件，由于Click事件本质上是Tap手势操作，我们可以通过GestureService/GestureListener组件监听并处理这个手势操作。下面将会示范如何通过GestureService/GestureListener组件处理Tap手势操作。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，打开CourseTimetablePage.xaml文件，在courseCollectionItemTemplate数据模板里添加GestureService/GestureListener组件，并把GestureListener的Tap事件处理程序设为CourseItem_Tap：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803334401.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在CourseTimetablePage.xaml.cs文件里创建这个事件处理程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080334465.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，sender参数指向的并非GestureListener对象本身，而是包含它的StackPanel，我们可以通过StackPanel的DataContext属性获取目标课程的课程名称，以便作为查询字符串的coursename参数的值传给CourseHubPage页。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了，终于可以按F5了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803347367.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击主菜单上的"课程表"进入课程表，在星期一、星期二和星期五这三天里分别创建一节"organizational behavior"课程：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803357857.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得注意的是，当你单击"上课地点"下面的编辑框，使之变成编辑状态时，整个页面会稍稍向上平移，这样做是为了避免软键盘打开时把编辑框挡住，这个特性是系统自带的，我们不必做任何事情就能拥有。不过，由于"确定"和"取消"两个功能还沿用着旧的设计，即通过传统的Button控件来实现，因此无可避免地被软键盘挡住了，我们应该改用上节课的新设计，即通过Application Bar按钮来实现，以便用户在输入完毕之后随时可以关闭页面，而不用先单击页面空白处关闭软键盘再单击确定按钮关闭页面，嗯，改造页面的工作就留给你当课后作业吧&amp;#8230;&amp;#8230;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;创建好课程之后，单击课程表上的任意一个课程打开CourseHubPage页：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803358348.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图可以看出，哪天有课已经正确显示了，下节课的上课日期也正确计算出来了。接着，我们来看看"今天笔记"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080336266.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在编辑框里输入一条笔记，然后单击旁边的新建按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803369121.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;再新建两条看看：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803373166.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;嗯，很好，前面创建的笔记在下面，后面创建的笔记在上面。接下来，我们看看"今天作业"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803389479.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;创建三项作业，并通过旁边的CheckBox控件把其中一项标记为已完成：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803391855.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，按两次Back键回到主菜单，然后单击"笔记本"进入笔记本：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803402312.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;长按第二条笔记打开上下文菜单，然后单击编辑菜单项打开编辑笔记的页面：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803412737.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;修改一下笔记内容，然后按确定保存并关闭页面：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803426574.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后按Back键回到主菜单，然后单击"作业本"进入作业本：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803438984.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图可以看出，作业的起止时间、完成状态都正确设置了。现在，长按第一项作业打开上下文菜单，然后单击编辑菜单项打开编辑作业的页面，把这项作业标记为已完成，并按确定关闭页面：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803433062.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后按Back键回到主菜单，然后单击"课程表"进入课程表，接着单击课程表上的任意一个课程打开CourseHubPage页，嗯，笔记内容和作业的完整状态都正确地更新了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803443835.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803454849.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;嗯，非常好，只是还有一个小小的地方需要改进的，前面输入数据的时候我是手动输入每个字符的，你知道，Windows Phone 7的模拟器不支持通过电脑的键盘进行输入，因此在测试的时候需要输入大量数据会很痛苦，我希望软键盘能够提供词条联想，比如说，当我输入"Org"三个字母时，它可以提示"Organizantion"给我选择，怎样才能做到呢？很简单，把编辑框的InputScope属性设为Text就可以了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803451228.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，当你在编辑框里输入数据时，软键盘上方就会显示相关的词条联想了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/20110324080346539.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;至此，Course Hub已经实现完毕了，但是，你有没有觉得它有点儿单调？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;动起来&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;单调吗？那就来点儿动感吧！首先是给课程表的课程列表添加&lt;a href="http://msdn.microsoft.com/en-us/library/ff941094(v=VS.92).aspx"&gt;Tilt Effect&lt;/a&gt;，它可以为控件交互产生一种特别的视觉反馈，感觉上就像控件后面垫了一层柔软的海绵，当你单击控件时，看起来就像你把控件放后面推了一下，当你释放控件时，它会自动复原。MSDN提供了Tilt Effect的&lt;a href="http://go.microsoft.com/fwlink/?LinkId=200720"&gt;示范代码&lt;/a&gt;以及把它移植到你的应用的&lt;a href="http://msdn.microsoft.com/en-us/library/ff941108(v=VS.92).aspx"&gt;教程&lt;/a&gt;，不过，如果你和我一样嫌这麻烦（说到底就是懒），可以直接使用&lt;a href="http://silverlight.codeplex.com/releases/view/60291"&gt;SL for WP Toolkit Feb 2011&lt;/a&gt;提供的Tilt Effect组件，不过，它还没修复我在&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html"&gt;第二节课&lt;/a&gt;里提到的bug，因此在使用之前我们需要按照那节课提到的办法修改一下SL for WP Toolkit的源代码。一切准备就绪之后我们就可以应用Tilt Effect了。打开CourseTimetablePage.xaml文件，找到pivotItemContentTemplate数据模板，然后在ListBox里添加TiltEffect.IsTiltEnabled附加属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803472699.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样就行了，是不是很简单呢，如果你希望把Tilt Effect应用到页面上的所有控件，你只需在PhoneApplicationPage里设置这个属性就可以了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来是Page Transitions，如果你有留意Windows Phone 7的相关视频，那么你应该已经看过Windows Phone 7的翻页效果，这是其中一种Page Transitions，怎么把它应用到我们的页面呢？非常简单，在你想应用翻页效果的页面根元素里添加如下代码：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803474618.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们可以把这段代码分别添加到CourseTimetablePage.xaml和CourseHubPage.xaml两个页面里。不过，仅仅这样还看不到效果，因为应用程序的RootFrame还只是一个普通的PhoneApplicationFrame，我们需要把它换成TransitionFrame。打开App.xaml.cs文件，在InitializePhoneApplication方法里把RootFrame初始化为TransitionFrame：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803471553.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，当用户单击课程表上的某个课程打开CourseHubPage页时，就会看到翻页效果了。除了翻页效果（Turnstile），SL for WP Toolkit还提供了滚动（roll）、旋转（rotate）、滑动（slide）和回旋（swivel）等四种效果，你可以通过SL for WP Toolkit附带的示范程序看看这些效果应用到页面时会是怎样的。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;最后是&lt;a href="http://msdn.microsoft.com/en-us/library/ff979339(v=Expression.40).aspx"&gt;FluidMoveBehavior&lt;/a&gt;，你知道吗，&lt;a href="http://channel9.msdn.com/shows/SilverlightTV/Creating-Rich-Interactions-Using-Blend-4-Transition-Effects-Fluid-Layout-and-Layout-States-Silverlig/"&gt;我第一次看到它产生的效果时就被它深深地吸引住了&lt;/a&gt;，我想，用"一见钟情"来描述这种感觉一点儿都不过分！你玩过新浪微博吗，当你发一条新的微博时，现有的微博会向下平移，与此同时，新的微博会在最上面逐渐显现出来，我希望为"今天笔记"实现这样的效果。右击"今天笔记"上的ListBox，选择Edit Additional Templates\Edit Layout of Items (ItemsPanel)\Create Empty：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803481487.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的Create ItemsPanelTemplate Resource对话框里输入模板名字，然后按OK关闭对话框：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803486961.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;进入模板的编辑状态之后，你会看到一个空的StackPanel，从Assets面板把FluidMoveBehavior拖到StackPanel里，确保FluidMoveBehavior处于选中状态，在Properties面板上把AppliesTo和Duration两个属性分别设为"Children"和"0.5"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803499893.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样就行了，是不是很简单呢，如果你希望为"今天作业"实现同样的效果，你不必重新创建一个模板，直接应用刚才那个就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803504811.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，如何让新的笔记会在最上面逐渐显现出来呢？如果你看过&lt;a href="http://channel9.msdn.com/shows/SilverlightTV/Creating-Rich-Interactions-Using-Blend-4-Transition-Effects-Fluid-Layout-and-Layout-States-Silverlig/"&gt;刚才那个视频&lt;/a&gt;，你可能会说，用最后介绍那个办法不就行了？非常遗憾，不行哦，LayoutStates这组可视状态是Silverlight 4的新特性，而Silverlight for Windows Phone 7是基于Silverlight 3的，尚未支持它们，因此无法通过它们实现我们想要的效果，怎么办？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;想想看，这个效果的本质是什么？一个动画！这个动画只做一件事情，使新的笔记逐渐呈现出来，换句话说，使它的Opacity属性的值从0逐渐变成1。现在，右击"今天笔记"上的ListBox，选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Edit Current：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803512270.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在Objects and Timeline面板上选中StackPanel，并在Properties面板上把它的Opacity属性的值设为0，因为新的笔记最初是不显示的。接着，在Objects and Timeline面板上创建一个名为FadeIn的动画，并把播放指针拖到0.5秒处：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803511124.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后在Properties面板上把StackPanel的Opacity属性的值设为100：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803513599.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，Opacity属性的类型是double，Expression Blend为了免除输入小数点的麻烦在界面上把这个值扩大了100倍，换句话说，在Properties面板上设100相当于在XAML里设1。好了之后关闭动画，然后在Assets面板上把ControlStoryboardAction拖到Objects and Timeline面板的StackPanel上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803527993.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最后，在Properties面板上把EventName和Storyboard两个属性的值分别设为Loaded和FadeIn：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803524023.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样就大功告成了。虽然这可以实现我们想要的效果，却无可避免地带来了一个新的问题：我怎样把这个效果应用到"今天作业"呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;最直接的办法是在"今天作业"上重做一遍上面的步骤，额，这显然不是一个办法，我不想重复劳动！这个时候，我们可以创建一个Behavior，把上面的逻辑封装起来，然后直接应用到StackPanel上，从而实现重用。现在，右击Utils文件夹，然后选择Add New Items，在弹出的New Item对话框里选择Behavior，并把它命名为FadeInWhenLoading：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803543891.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们知道，FrameworkElement类是整个继承体系里最先同时拥有Opacity属性和Loaded事件的，因此，我们需要把FadeInWhenLoading类的声明改成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803547239.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在OnAttached方法里把Opacity属性的值设为0，并订阅Loaded事件：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803549681.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而AssociatedObject_Loaded方法所做的事情仅仅是创建并播放使Opacity属性的值从0逐渐变成1的动画：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803558776.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最后，在OnDetaching方法里取消订阅Loaded事件：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803559299.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后，重新编译一下，然后右击"今天笔记"上的ListBox，选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Edit Current进入模板编辑状态，撤销前面所做的操作（包括设置Opacity属性的值、创建动画以及应用ControlStoryboardAction），然后在Assets面板上把FadeInWhenLoading拖到Objects and Timeline面板的StackPanel上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803562057.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;大功告成！现在，你可以把FadeInWhenLoading直接应用到"今天作业"上了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;动画效果是Windows Phone 7用户体验的一个重要组成部分，它使得应用程序不再单纯地从某个状态突然跳到另一个状态，比如Tilt Effect，它使得控件不再只有按下和释放两种状态，当你单击控件时，它会向你单击的那个地方倾斜，非常有趣；又比如Page Transitions，前进到一个页面和返回相同页面会产生不同的动画效果，想象你看书的时候翻到下一页和回到上一页的情景，这使你不会在这个过程中迷失；再比如FluidMoveBehavior，它使你清楚的了解到当前查看的内容是如何变化的，所有这些，目的只有一个，提高用户体验，当然，前提是运用得当。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;下课了&amp;#8230;&amp;#8230;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201103/201103240803579417.jpg" alt="" /&gt;&lt;/p&gt;&lt;p&gt;特别感谢PRO为本系列文章精心打造了一个WP7手机外壳，并合成了上面这幅图。&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/1993368.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/03/24/1993368.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2011/01/11/1933165.html</id><title type="text">WP7有约（三）：课堂重点</title><summary type="text">WP7有约（三）：课堂重点Written by Allen LeeCrawling in my skin, these wounds they will not heal. Fear is how I fall, confusing what is real.– Linkin Park, Crawling记笔记 俗话说：好记性不如烂笔头。当然，这并不是说我们的脑子不好使，也不是叫我们不要用脑子记...</summary><published>2011-01-11T11:32:00Z</published><updated>2011-01-11T11:32:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2011/01/11/1933165.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2011/01/11/1933165.html"/><content type="html">&lt;p&gt;&lt;span style="color:#0070c0; font-size:20pt"&gt;&lt;strong&gt;WP7有约（三）：课堂重点&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee&lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Crawling in my skin, these wounds they will not heal. Fear is how I fall, confusing what is real.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;&amp;#8211; Linkin Park, Crawling&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;记笔记&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;俗话说：好记性不如烂笔头。当然，这并不是说我们的脑子不好使，也不是叫我们不要用脑子记东西，而是提醒我们解放脑力，让大脑从事更有价值的思考。因此，这节课我们将会创建一个笔记本，用来记录课堂重点，但是，我们需要什么样的笔记本呢？我曾经在&lt;a href="http://book.douban.com/subject/1135754/"&gt;《你的灯亮着吗？》&lt;/a&gt;里读到这样一句话：&lt;span style="text-decoration:underline"&gt;如果某人能够解决这个问题，但是他本人却不会遇到这一问题时，那么你们首先要做的就是让他也感受到这个问题。&lt;/span&gt;最近公司来了一批校招生，我找了个机会混进去听了一节入职前的技术培训，我想知道在课堂上把手机掏出来记笔记是一种什么样的感觉。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;在课堂上，每当我想记点什么时，就会不自觉地拿起纸笔而不是手机，而且，用手机记笔记远没用纸笔来得随意自如。随后，我找了一些大学生和中学生，分别了解一下他们记笔记的情况，结果发现，他们记笔记的方式真是多种多样，有的直接记在书上，有的记在专门的笔记本上，有的记在练习册上，有的记在卷子上，有的甚至用手机把老师的板书直接拍下来&amp;#8230;&amp;#8230;不难看出，他们的做法是怎么方便就怎么记，就目前而言，企图用一个手机应用来取代他们现有的做法显然是不现实的，也没必要，用户有权选择他们认为适合的做法，而我们的职责只是提供必要的帮助和支持。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;那么，我们可以提供什么样的帮助和支持呢？想想看，现有的自由零散的做法会导致什么问题呢？最直接的影响是很难快速找到想要的内容，因为它们可能遍布各处，这种时候要是有个索引或者目录什么的就好了&amp;#8230;&amp;#8230;Bingo！我们可以创建一个应用，帮助用户建立这个索引，虽然用户也可以另外找本小册子建立索引，但我们可以通过一个标签系统帮助用户快速找到相关的内容。这样，用户既可以保留现有的自由的记笔记习惯，又可以获得新的有序的管理效果。那么，用户应该在何时以及如何建立这个索引呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;当然是越早越好！比如说，用户可以在每晚做完作业之后稍稍整理一下笔记，然后为它们创建一些条目并贴上标签。用户不必为所有笔记创建条目，可以挑选重要的来创建，这个过程本身就可以加深对知识的理解和巩固对知识的记忆。至于条目的内容，用户可以引用课本或者老师板书的原话，也可以用自己的话来概括复述，还可以直接引用课本或者练习册的页码和位置（段落、题号或者标记）等等，这个过程可以帮助用户熟悉如何根据条目的内容找到对应的笔记。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，用Visual Studio打开项目，在Models文件夹里创建一个Note类，并让它继承NotificationObject类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901215657.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;根据前面的讨论，Note类应该包含以下四个属性：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;     &lt;colgroup&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;/colgroup&gt;     &lt;tbody valign="top"&gt;         &lt;tr style="background: #9bbb59"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性名字&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性类型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性描述&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Id&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Guid&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;唯一标识&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Course&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;string&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;课程名称&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Content&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;string&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;笔记内容&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Tags&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;string&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;笔记标签&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;     &lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;其中，Id是只读属性，根据&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html"&gt;上节课&lt;/a&gt;的经验，我们需要给它创建以下两个构造函数：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901219769.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;它们分别用于新建和编辑两种情景。Course、Content和Tags三个属性的set访问器均需调用RaisePropertyChanged方法，此方法是从NotificationObject类继承过来的。Tags属性可以包含一个或多个标签，当Tags属性包含多个标签时，标签与标签之间将会使用逗号（,）进行分隔。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;数据存储方面，我们将会直接使用上节课重构的结果&amp;#8212;&amp;#8212;JsonDataStore类。打开App.xaml.cs文件，在App类里创建一个NoteStore静态属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190121608.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接下来，是时候考虑一下用户体验了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;设计用户体验&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们知道，每门课都有自己的笔记，就像每门课都有自己的作业一样，所以这里将会仿效作业本的做法，利用Pivot控件的特点，让每个Pivot项显示一门课程的笔记。现在，切换到Expression Blend，创建一个Windows Phone Pivot Page，并把它命名为NoteBookPage.xaml，完了之后把Pivot控件的Title属性设为"笔记本"，把两个Pivot项的Header属性分别设为"销售心理学"和"行为金融学"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190122193.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，Application Bar上的按钮没有设置图标，这是因为我没有找到合适的，下次有时间我自己设计一个放上去。那么，标题下面这么大的一块空位应该怎么安排呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，有两点我们是明确的，第一，我们需要列出笔记，第二，我们需要提供某种方式让用户切换标签。根据以往的经验，ListBox最适合用来列出笔记，至于切换标签的方式，我觉得SL for WP Toolkit的ListPicker也是一个不错的选择。此时，我的脑子里浮现出第一个设计：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190123127.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然而，很遗憾，我对这个设计并未感到满意。第一，&lt;span style="text-decoration:underline"&gt;在手机屏幕这样的有限空间里，我们应该始终坚持把尽可能多的空间留给最重要的内容&lt;/span&gt;，ListPicker作为一个辅助元素应该只在用户需要切换标签的时候才显示，否则把空间腾出来，这样可以显示更多笔记。第二，笔记本和作业本、课程表有着类似的布局设计，但ListPicker的存在破坏了布局设计的一致性，这点我实在无法容忍。那么，怎样才能让ListPicker"呼之则来挥之则去"？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们可以调整ListBox的大小，使之充满整个Pivot项，并把ListPicker藏在屏幕下方外面，然后在Application Bar上放置一个按钮，当用户单击这个按钮时，ListPicker将会从屏幕下方外面向上平移，直至屏幕底部：&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901248425.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，用户就可以通过ListPicker切换标签了。此时，我的脑子里冒出一个问题，为什么不直接以这种方式显示标签列表，而是大费周章地通过ListPicker打开一个新的页面呢？显然，我找不到合适的理由说服自己把ListPicker留下，同时，这个问题也给我带来了新的灵感。我们可以让显示标签的ListBox取代ListPicker，当用户单击Application Bar上的按钮时，ListBox将会从屏幕下方外面向上平移，直至覆盖显示笔记的ListBox为止，当用户选好标签之后，显示标签的ListBox将会向下平移，直至屏幕下方外面为止。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，把ListPicker删除，然后把一个ListBox添加到LayoutRoot，并调整它的大小，使之充满整个屏幕（覆盖后面的Pivot控件）。此时，Objects and Timeline面板上面的对象应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901241423.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，把它向下平移，直至它的顶部和Pivot控件的底部重叠为止：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901257769.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，它的Height、VerticalAlignment和Margin三个属性都会自动做出相应的调整：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901254912.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接下来，我们将会为它创建动画。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;单击Objects and Timeline面板上的+按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901259896.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的Create Storyboard Resource对话框里输入一个名称，然后按OK关闭对话框：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901256831.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901264747.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;确保ListBox处于选中状态，把播放指针拖到0.5秒的为止，然后把ListBox向上平移，直至和Pivot控件完全重叠为止。此时，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901278062.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，为什么只有结束时间的关键帧？你也可以手动设置开始时间的关键帧，如果没有设置，那么对象原本的状态将会默认为开始时间的关键帧。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;当用户选好标签之后，我们需要把ListBox隐藏起来，不难想象，隐藏动画其实是显示动画的反转，那么，如何创建反转动画呢？非常简单，单击+按钮旁边的箭头，然后选择Duplicate：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901273012.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Expression Blend会为你创建一个ShowTagsStoryboard_Copy1动画。单击+按钮旁边的箭头，然后选择Rename把它重命名为HideTagsStoryboard。接着，再次单击+按钮旁边的箭头，然后选择Reverse反转动画。好了之后单击+按钮旁边的X按钮关闭Storyboard。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;那么，我们如何触发这些动画？前面说过，当用户单击Application Bar上的按钮时，ListBox将会显示，这个比较简单，只需在按钮的事件处理程序里调用ShowTagsStoryboard的Begin方法就可以了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901281584.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而当用户选好标签之后，ListBox将会隐藏，这个可以通过Expression Blend提供的ControlStoryboardAction来实现。打开Assets面板，选择Behaviors，然后把ControlStoryboardAction拖到Objects and Timeline面板的ListBox上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901308877.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901304384.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;确保ControlStoryboardAction处于选中状态，在Properties面板上把EventName和Storyboard两个属性的值分别改为MouseLeftButtonUp和HideTagsStoryboard：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901302715.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，当用户在ListBox里选好标签并松开手时就会触发HideTagsStoryboard。看到这里，你可能会问，为什么前面不直接在Application Bar的按钮上使用ControlStoryboardAction？这是因为Application Bar并非Silverlight的一部分，你不可以把它和我们平时接触到的Silverlight对象等同起来。事实上，如果你试图把ControlStoryboardAction拖到ApplicationBarIconButton上，Expression Blend会提示你"Not a valid drop target"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901316270.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接下来，我们将会为两个ListBox定制数据模板。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，通过Data面板导入以下两个XML文件：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901311253.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901314601.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Data面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901324153.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;把Data面板上的NoteCollection拖到显示笔记的ListBox上，然后进入列表项模板的编辑状态，把StackPanel的Margin属性、TextBlock的FontSize属性和TextBlock的TextWrapping属性分别设为PhoneTouchTargetOverhang、PhoneFontSizeNormal和Wrap：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901348448.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后退出列表项模板的编辑状态。接着，把TagCollection拖到显示标签的ListBox上，然后进入列表项模板的编辑状态，把StackPanel的Margin属性、TextBlock的FontSize属性和TextBlock的TextWrapping属性分别设为PhoneTouchTargetOverhang、PhoneFontSizeLarge和Wrap：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901344271.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接下来干嘛？你懂的！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;打开MainPage.xaml，添加一个菜单项，并让它导航至NoteBookPage页：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901357793.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，按F5吧：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901365742.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击笔记本：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901362611.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击Application Bar上的按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901378608.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;啊！忘记把ListBox的Background设成不透明了！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;退出应用程序。把ListBox的Background改为PhoneBackgroundBrush。而动画方面，0.5秒感觉有点长，我们可以把它改为0.25秒：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901374.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此外，还有一个地方值得改善的，我希望ListBox出来的时候能够有一种逐渐慢下来的感觉，而离开的时候则相反，逐渐快起来。这可以通过缓动函数（easing function）来实现。单击Objects and Timeline面板上的Open a Storyboard按钮，然后选择ShowTagsStoryboard：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901388019.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;展开ListBox节点，确保下面的RenderTransform处于选中状态：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901391508.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后在Properties面板上把EasingFunction设为Cubic Out：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901393983.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后仿照上述步骤把HideTagsStoryboard的EasingFunction设为Cubic In。现在，你可以按F5看看修改后的效果了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来，是时候考虑一下笔记本的相关操作了。我们知道，课程表通常都是整个创建的，而作业通常也一次过把一门课当天要做的都记下来，对于这种集中式批量操作来说，提供保存所有更改和撤销所有更改是有必要的，但笔记本就不同了，里面的内容很可能分散在不同的时间点记录，没有太明显的集中式批量操作，如果我们遵循课程表和作业本的套路，要么用户不得不在每次记笔记时都额外执行一次保存操作，要么用户不得不承担最后可能忘记统一保存而丢失数据的风险。因此，我打算把五项操作简化为新建、编辑和删除三项，并在用户执行每项操作之后自动保存数据，其中，新建将会以ApplicationBarIconButton的方式放在Application Bar上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901406642.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而编辑和删除则放在上下文菜单里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901416293.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;根据之前的经验，我们需要一个新的页面来处理新建和编辑操作：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901423653.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，我们应该如何设计这个页面呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;想想看，Note类的哪些属性和用户无关？Id。哪些属性无需劳烦用户处理？CourseName。那么剩下的Content和Tags两个属性就应该出现在这里了。毫无疑问，TextBox完全能够胜任显示和编辑这两个属性的工作：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901433587.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得注意的是，这里不再通过普通的Button控件来提供"确定"和"取消"两项功能，而是通过Application Bar上的按钮来提供，为什么呢？当用户输入完毕之后，软键盘可能处于开启状态，它会遮盖普通的Button控件，这意味着用户就不得不先单击一下页面上的空白处关闭软键盘，再单击Button控件关闭页面，而Application Bar不会被软键盘遮盖，这意味着用户可以在输入完毕之后直接单击上面的按钮关闭页面。你知道吗，这个小小的简化可以极大地提升用户体验，之前测试课程表和作业本的时候，这个不必要的步骤曾多次让我误触TimePicker/DatePicker控件的有效范围，导致新的页面被打开，我对此深感厌恶，你知道，当用户对软件的操作感到厌恶时，后果将会很严重！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，回到NoteBookPage页，为Application Bar上的新建按钮添加一个事件处理程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901432474.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后按F5：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190143456.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图可以看出，软键盘和Application Bar是并列一起的，所以我可以在任何时候单击Application Bar上的按钮。此外，当我在TextBox里输入较长的内容时，TextBox还会自动调整自身的高度，以便完整显示我输入的内容。当我单击第二个TextBox进行输入时，整个页面将会稍稍向上平移，这样做的好处非常明显&amp;#8212;&amp;#8212;避免软键盘遮盖TextBox！从这里不难看出，WP7在用户体验上的设计确实很体贴！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;连接前端和后端&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来，我们要为这些用户界面创建对应的ViewModel类。我们知道，整个笔记本就是一个Pivot控件，而每门课程的笔记则对应一个Pivot项，这个结构和上节课的作业本类似，于是，我们可以仿效上节课的做法，分别创建NoteBookViewModel和NoteListViewModel两个类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901445440.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901448471.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;由于每个Pivot项都包含了一个标题和一组笔记，NoteListViewModel类自然需要提供两个对应的属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901449310.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得注意的是，这里不直接使用ObservableCollection集合，而是和第一节课的课程表一样，通过CollectionViewSource来提供集合视图，这样做的好处是我们只需指定过滤条件，剩下的事情CollectionViewSource会代为处理，而无需我们亲自出手：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901443737.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而NoteBookViewModel类则和上节课的作业本一样，直接使用ObservableCollection集合：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901448721.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;并根据课程表里的数据创建对应的NoteListViewModel对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901458372.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;有了ViewModel类，我们就可以着手处理数据绑定了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，打开NoteBookPage.xaml文件，切换到XAML模式，在页面的资源字典里添加两个数据模板：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901452799.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，把它们应用到Pivot控件上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901459735.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;把LayoutRoot的DataContext属性去掉，然后在代码隐藏文件的构造函数里设置DataContext属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901458306.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，按F5吧。在打开笔记本之前，我们得先新建一些课程，否则Pivot控件不会创建任何Pivot项的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190146748.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后就可以打开笔记本了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190146715.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当然，现在的笔记本既没有笔记也不能创建笔记，因为这部分功能还没实现呢！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;新建和编辑笔记的工作是由NewOrEditNotePage页负责的，根据上两节课的经验，我们需要创建NewOrEditNoteViewModel、NewNoteViewModel和EditNoteViewModel三个类，但我已经厌倦了每次都要手工创建这么多类，而且还有这么多重复的代码，所以这次我要对这部分进行重构。在ViewModels文件夹里创建一个NewOrEditItemViewModel泛型类，并让它继承NotificationObject类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901465699.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;仔细观察NewOrEditCourseViewModel和NewOrEditAssignmentViewModel两个抽象类，不难发现，它们的有效成分是页面标题、Model类的实例和提交数据的方法。页面标题很好处理，一个普通的类型为string的Title属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901462634.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Model类的实例有点棘手，因为我们有3个不同的Model类，怎么处理？我们有两个选择，一个是把属性的类型声明为object，另一个就是这里采用的做法&amp;#8212;&amp;#8212;泛型：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901475981.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这也正是我把NewOrEditItemViewModel类声明为泛型类的缘由。我们知道，每个Model类的数据都会提交到不同的地方，我们显然不能把这部分代码固化在NewOrEditItemViewModel类里，所以我决定通过委托把代码外包出去：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901472601.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最后，我们需要在构造函数里初始化这三个成分：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901475948.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，我们如何使用这个类呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;打开NewOrEditNotePage.xaml.cs文件，重写OnNavigatedTo方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901473963.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个是我们根据查询字符串初始化DataContext属性的基本套路。当action的值是new时，初始化DataContext属性的代码应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/2011011119014727.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而当action的值是edit时，初始化DataContext属性的代码应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901481946.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，下次我们再有新的Model类就可以直接创建ViewModel类的实例了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;看到这里，你可能会问，代码24那个套路每次都是一样的，应该可以处理一下吧？嗯，可以的。我们有两种处理方案，第一种方案是创建一个NewOrEditItemViewModelFactoryBase抽象类，并在里面使用模板方法模式（Template Method Pattern）处理那个套路，这样做的代价是我们需要为每个Model类创建一个对应的工厂类。如果你不喜欢这种做法，你可以选择第二种方案，创建一个NewOrEditItemPage类，按照代码24重写OnNavigatedTo方法，然后应用模板方法模式，这样做的代价是我们需要让每个新建/编辑页面继承这个类。无论我们选择哪种方案，有一点是可以肯定的，那就是NewOrEditItemViewModel类的三个成分似乎无法避免，因为这些工作始终要做，而我们从一种方案改成另一种方案只不过是把这些工作从一个地方挪到另一个地方罢了。如果你确实希望减轻这些工作，（理论上）也不是不可能，不过你得做好心理准备，因为你需要创建一个足够灵活的子系统，并且提供充足的元数据，这些数据包括每个Model类在不同状态下分别对应的页面标题、新建Model类的实例需要初始化哪些属性以及这些属性的数据来自哪里或者如何计算、克隆现有Model类的实例的方法是哪个、数据提交到哪里以及调用哪个方法、提交数据的是否需要同时保存到独立存储区等等等等。当我写到这里的时候，我已经隐约感觉得出这将是个很复杂的子系统，如果你真的打算实现这个子系统，那么请你先把右手抬起来，捂在左边胸口，然后问问自己：&lt;/p&gt;&lt;ol&gt;     &lt;li&gt;会有人愿意负责提供这些数据吗？如果有，会是谁呢？     &lt;/li&gt;     &lt;li&gt;会有人愿意负责维护这个子系统吗？如果有，会是谁呢？     &lt;/li&gt;     &lt;li&gt;当你的程序用上这个子系统之后，你能得到什么实质的好处？     &lt;/li&gt;     &lt;li&gt;这些好处能否抵消提供数据和维护子系统的付出？     &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;如果你没有自欺，你的心将会告诉你这个决定是否值得。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;创建好ViewModel类之后，我们就可以着手处理它和页面之间的关联了。首先是设置数据绑定，需要设置的控件以及对应的绑定表达式如下表所示：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;     &lt;colgroup&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;col style="width:288px"&gt;&lt;/colgroup&gt;     &lt;tbody valign="top"&gt;         &lt;tr style="background: #9bbb59"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;描述&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;类型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;绑定表达式&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;页面标题&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;TextBlock&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Text&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;{Binding Title}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;笔记内容&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;TextBox&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Text&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;{Binding Item.Content, Mode=TwoWay}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;笔记标签&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;TextBox&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Text&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;{Binding Item.Tags, Mode=TwoWay}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;     &lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，为Application Bar上的两个按钮创建事件处理程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901489961.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;页面的功能有了，可打开页面的功能还没好呢！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们知道，NewOrEditNotePage页的入口点有两个，而且都在NoteBookPage页上，一个是Application Bar上的新建按钮，另一个是上下文菜单里的编辑菜单项。当用户单击新建按钮时，我们需要告诉NewOrEditNotePage页当前的课程是什么，但是，我们从哪里获取这个信息？办法有很多种，其中一种是在NoteBookViewModel类里创建一个SelectedNoteList属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901488848.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;并把它绑到Pivot控件的SelectedItem属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901491323.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后在新建按钮的事件处理程序里通过SelectedNoteList的Header属性获取课程名称：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901494322.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当用户单击编辑菜单项时，我们需要告诉NewOrEditNotePage页笔记的Id是什么。我们知道，上下文菜单是嵌在列表项里的，这意味着它能从列表项那里继承DataContext属性的值，而这个值正是当前选中的笔记，所以我们可以通过菜单项的DataContext属性获取笔记的Id：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190149701.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;类似地，删除操作也是通过相同的方式获取当前选中的笔记，然后把它删除：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190150352.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了，不知不觉又到看效果的时候了！打开笔记本：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901504223.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击新建按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901514157.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;输入笔记内容和笔记标签，然后单击确定返回：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901512695.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，长按笔记打开上下文菜单：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901526009.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;选择编辑：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190152404.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;嗯？我刚才没有输入标签？还是我现在眼花看错？为什么标签没有保存？现在，重新输入标签，单击页面上的空白处，单击确定返回，然后再次编辑笔记：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901532846.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;哈！这次有了！怎么回事？！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;这个时候，我的脑子里突然蹦出一个问题，当TextBox处于编辑状态时，单击确定按钮会不会触发TextBox的LostFocus事件？为了回答这个问题，我做了一个试验，结果发现，当TextBox处于编辑状态时，单击页面上的按钮或者空白处都能触发TextBox的LostFocus事件，而单击Application Bar上的按钮却不会，在这种情况下，TextBox的内容不会提交！这个问题不难解决，我们只需在调用Submit方法之前让TextBox失去焦点就行了，要实现这个效果，最简单的办法是让页面获得焦点：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901536957.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这等效于单击页面上的空白处。不幸的是，这个办法只在新建笔记时有效，我不知道为什么，看来我们只能走别的路子了。在Silverlight里，TextBox的Text属性在两种情况下会更新绑定源，第一种情况我们刚才试过了，结果你也知道了，第二种是手动更新，这里的手动更新并不是指把数据从Text属性手动复制到Note对象的对应属性，而是告诉Text属性的绑定表达式把当前数据更新回绑定源。我们知道，当用户单击Application Bar上的确定按钮时，最多只有一个TextBox获得焦点，其它TextBox会因为失去焦点自动更新，因而没有必要对每个TextBox进行手动更新。那么，如何才能得到获得焦点的控件？FocusManager类提供了一个GetFocusedElement静态方法，它返回获得焦点的控件，如果这个控件是TextBox，我们就告诉Text属性的绑定表达式把当前数据更新回绑定源：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901535288.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;重新运行应用程序，这次没问题了。虽然这个问题我们自己也可以解决，但我个人认为这是系统应该考虑到的问题，即使普通按钮和Application Bar上的按钮有着本质的区别，容许这种行为上的不一致会为开发者带来不便和困惑。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;标签&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;到目前为止，我们的笔记本只不过是一个很普通的笔记本，虽然我们提供了与标签相关的用户界面，但这部分功能还没真正实现出来。那么，如何实现这部分功能？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们知道，当用户单击Application Bar上的显示标签按钮时，显示标签的ListBox将会滑出来，里面列出当前课程的标签，用户可以从中选择一个标签，此时，ListBox将会滑出去，笔记本的内容也将根据选中的标签进行筛选。从这里不难看出，我们需要在NoteListViewModel类里添加两个属性，一个用于存放当前课程的标签，另一个用于存放当前选中的标签：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901537763.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，在使用Tags属性之前，我们需要在构造函数里初始化它，即创建一个空的集合对象。接着，把它们分别绑到ListBox的ItemsSource和SelectedItem两个属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901542747.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，当用户选好标签之后，我们如何筛选笔记？还记得我们是如何根据课程名称筛选笔记的吗？我们直接把课程筛选条件告诉CollectionViewSource，当数据源发生改变时，CollectionViewSource将会自动筛选，因此，我们不妨考虑把标签筛选条件整合进去，让CollectionViewSource一并处理：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901544350.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，当用户还没选择任何标签时，SelectedTag属性的值为null，此时，我们应该按照不做标签筛选的情况处理，否则看看笔记的标签是否包含用户选中的标签。看到这里，你可能会问，当用户选择一个标签时，数据源并未发生任何改变啊，CollectionViewSource应该不会自动筛选吧？这个问题问得好！事实上，它不会自动筛选，我们需要手动刷新一下它生成的视图：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901544873.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，如何初始化标签列表？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;想想看，什么时候需要初始化标签列表？每次打开笔记本的时候肯定需要初始化标签列表，但除此之外呢？当用户新建或编辑笔记时，可能引入新的标签；当用户编辑或删除笔记时，可能删除现有标签，这些都会导致重新计算标签列表。既然计算标签列表的代码需要在这么多地方使用，我们当然应该把它提取到一个单独的方法里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901547664.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;计算标签列表的思路非常简单，首先，选取当前课程的笔记（忽略没有标签的），接着，从中提取标签，为了避免前/后空格的影响，这里使用Trim方法做了处理，然后，去掉空字符串以及重复的标签，最后，把标签添加到Tags属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，请思考一下，我们应该在哪调用ComputeTags方法？有些同学可能会说，这还不简单，分别在NoteListViewModel类的构造函数和三个操作的事件处理程序里调用不就行了？在NoteListViewModel类的构造函数和删除操作的事件处理程序里调用是没问题的，但在新建和编辑两个操作的事件处理程序里调用就有问题了，为什么呢？举个例子吧：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901559267.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;你觉得上面代码的最后一行是在NewOrEditNotePage页显示之前还是之后执行呢？答案是之前，这意味着标签列表的计算在用户编辑笔记之前就完成了，这显然不是我们期望的效果。怎么办？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;想想看，每次从NewOrEditNotePage页返回都会发生什么事呢？触发NoteBookPage页的Loaded事件！于是，我们可以把代码39里的最后一行放在Loaded事件处理程序里，不过，这样做会导致一个问题，每次从主菜单打开NoteBookPage页时，当前课程的标签列表会被计算两次，第一次是在NoteListViewModel类的构造函数里，第二次是在Loaded事件处理程序里，因为NoteBookPage页每次显示都会触发Loaded事件。怎么处理这个问题？最简单的办法是通过一个bool变量区分NoteBookPage页是否已经打开过。不过，既然我们的目的只是为了在标签发生改变时做些事情，为什么不直接监听Note对象的PropertyChanged事件呢？要实现这个效果，有三个事儿需要我们做的：&lt;/p&gt;&lt;ol&gt;     &lt;li&gt;监听现有Note对象的PropertyChanged事件。     &lt;/li&gt;     &lt;li&gt;监听App.NoteStore.Items的CollectionChanged事件，一旦有新的Note对象添加进来就监听它的PropertyChanged事件。     &lt;/li&gt;     &lt;li&gt;监听App.NoteStore.Items的CollectionChanged事件，一旦现有的Note对象被删除就移除PropertyChanged事件的监听。     &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;我们可以在PropertyChanged事件处理程序里调用ComputeTags方法。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;虽然这种做法听起来有点复杂，但它避免了第一种做法的问题。本质上，这两种做法是等效的，只是一个在前台处理，另一个在后台处理，而正是这个角度的转变让我们对它们有了更进一步的了解。想想看，用户并非每次新建/编辑笔记之后都会单击Application Bar上的显示标签按钮，一个比较常见的使用情景用户把当天的笔记都输入了，然后通过标签的筛选来复习特定的内容，这样的话，在用户每次新建/编辑笔记之后重新计算标签列表显然没有必要。事实上，如果用户没有单击Application Bar上的显示标签按钮，我们根本没有必要计算标签列表，换句话说，我们可以把代码39里的最后一行放在显示标签按钮的Click事件处理程序里，从而实现按需计算：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190155106.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要注意的是，当用户单击Application Bar上的显示标签按钮时，如果用户还没创建任何课程，直接调用ComputeTags方法将会引发异常，所以我们需要在调用之前判断一下。不过，这种做法也有个问题，试想一下，如果用户多次单击Application Bar上的显示标签按钮，其间没有新建/编辑任何笔记，那么，除了第一次计算标签内容是必要的，后面几次都是多余的。那么，如何才能避免多余的计算？看到这里，你可能会说，为什么不把后两种做法结合起来试一下呢，比如说，我们可以通过一个bool变量标识是否需要计算，然后在PropertyChanged事件处理程序里把它的值设为true，而在ComputeTags方法里，仅当这个变量的值为true时才执行计算，执行完毕之后把它的值设为false。嗯，这个主意不错，我就把它留给你当课后作业吧。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了，不知不觉又到看效果的时候了！按F5运行应用程序，新建一条笔记：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901561120.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击Application Bar上的显示标签按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901563595.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击页面空白处收回标签列表。再新建一条笔记：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901561577.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这次，我们给它两个标签，其中一个标签是现有的，另一个是新的，并且分隔符后面有个空格。单击确定返回，然后单击Application Bar上的显示标签按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901579593.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;再新建一条笔记：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901575939.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 42&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，单击Application Bar上的显示标签按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901582317.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 43&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;选择buying behavior：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901582808.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 44&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;再次单击Application Bar上的显示标签按钮，选择sales strategy：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901591903.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 45&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;非常好！不过，现在有个问题，我想显示所有笔记怎么办？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;没问题，我们可以在计算标签列表的时候加插一个"特殊"的标签：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901592742.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 42&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;并在筛选的时候进行"特殊"处理：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111901596853.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 43&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，重新运行应用程序，分别为两个课程新建一些笔记：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111902005707.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 46&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111902002610.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 47&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，单击Application Bar上的显示标签按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190200625.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 48&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;选择disposition effect：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111902013034.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 49&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，切换到sales psychology课程，单击Application Bar上的显示标签按钮，选择sales strategy：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111902026033.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 50&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;再次单击Application Bar上的显示标签按钮，选择（全部）：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111902027918.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 51&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，切换回behavioral finance课程：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111902027919.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 52&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;怎么回事？！我们刚才已经做了筛选啊！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;原来，当我们切换课程时，ListBox的ItemsSource属性发生改变，导致ListBox的SelectedItem属性被重设为null，而NoteListViewModel对象的SelectedTag属性和ListBox的SelectedItem属性是双向绑定的，因而也被重设为null了。ListBox的SelectedItem属性被重设为null是对的，因为新的数据源不一定包含SelectedItems属性的值，但把NoteListViewModel对象的SelectedTag属性也重设为null就不对了，因为同一个NotelistViewModel对象的Tags属性肯定包含SelectedTags属性的值，因此，SelectedTag属性的set访问器应该忽略这个重设：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190440371.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 44&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;重新运行应用程序，重新执行一次上面的测试，嗯，这次没问题了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;命令与行为&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们知道，WPF和最新的Silverlight 4都支持命令绑定，比如说，Button控件有一个Command属性和一个CommandParameter属性，前者用于绑定实现ICommand接口的对象，后者用于绑定传给前者的参数，但SL for WP却只有一个ICommand接口，这意味着我们无法为按钮设置命令，而Application Bar上的按钮这种异类就更不用说了。幸亏&lt;a href="http://compositewpf.codeplex.com/"&gt;Prism&lt;/a&gt;为我们带来了ApplicationBarButtonCommand（使用之前请先引用Microsoft.Practices.Prism.Interactivity.dll类库），它能让我们为Application Bar上的按钮设置命令。下面，我们拿NewOrEditNotePage页来示范它的用法。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;在设置命令之前，我们得先有个命令，而命令通常是由ViewModel类提供的。打开NewOrEditItemViewModel.cs，在NewOrEditItemViewModel类里添加一个SubmitCommand属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904403403.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 45&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，我们应该如何初始化它？一般的做法是创建一个SubmitCommand类，并让它实现ICommand接口，然后在NewOrEditItemViewModel类的构造函数里把SubmitCommand类的实例赋给SubmitCommand属性。如果你不嫌麻烦的话，你可以这样做，不过，由于创建命令对象的需求非常普遍，Prism为我们带来了DelegateCommand泛型类，我们只需把提交数据的代码传给它的构造函数就可以了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/2011011119044023.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 46&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，把_submit私有字段以及在构造函数里初始化它的代码删除，因为我们不再需要它了。删除之后，把Submit方法改成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904405006.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 47&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;换句话说，原来的_submit私有字段被现在的SubmitCommand属性取代了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，打开NewOrEditNotePage页，从Assets面板上把ApplicationBarButtonCommand拖到Objects and Timeline面板的PhoneApplicationPage上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904419433.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 53&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904417764.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 54&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在Properties面板上把ButtonText属性的值设为"确定"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904412748.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 55&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击CommandBinding右边的小正方形，并选择Custom Expression：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904419367.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 56&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的Custom expression对话框里输入"{Binding SubmitCommand}"并按回车：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904412715.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 57&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;用同样的办法把CommandParameterBinding设为"{Binding Item}"。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;那么，用户提交数据之后如何返回？这个时候就轮到ApplicationBarButtonNavigation上场了。从Assets面板上把ApplicationBarButtonCommand拖到Objects and Timeline面板的PhoneApplicationPage上，此时，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190442664.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 58&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在Properties面板上把ButtonText和NavigateTo两个属性的值分别设为"确定"和"#GoBack"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/20110111190443598.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 59&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，"#GoBack"是一个硬性规定的特殊值，当我们把NavigateTo属性的值设为"#GoBack"时，ApplicationBarButtonNavigation会调用NavigationService.GoBack方法返回，而当我们把NavigateTo属性设为XXX.xaml时，它会调用NavigationService.Navigate方法导航至对应的页面。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;那么，Text属性更新绑定源的问题呢？难道Prism也提供了相应的组件？没有，这次我们得亲自出手了。右击Utils文件夹，然后选择Add New Items，在弹出的New Item对话框里选择Behavior，并把它命名为AppBarButtonUpdateSource：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904442483.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 60&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们知道，Application Bar上的按钮并非Silverlight的一部分，因此Behavior无法直接作用于它，而Silverlight里只有PhoneApplicationPage类提供了访问Application Bar的方法，因此我们需要把AppBarButtonUpdateSource的目标类型改为PhoneApplicationPage：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904447467.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 48&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这也正是我们把ApplicationBarButtonCommand和ApplicationBarButtonNavigation拖到PhoneApplicationPage上的原因。那么，如何才能找到Application Bar上的按钮？Prism为我们带来了FindButton扩展方法，可以通过按钮的文字来查找，因此，我们需要创建一个ButtonText属性和一个_button私有字段，前者用于指定待查找按钮的文字，后者用于保存找到的按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904442450.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 49&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;FindButton扩展方法需要一个实现IApplicationBar接口的对象，而能够提供这个对象的只有PhoneApplicationPage对象，后者可以通过Behavior的AssociatedObject属性访问。于是，我们可以在OnAttached方法里初始化_button私有字段：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904442102.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 50&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;找到按钮之后，我们需要把更新绑定源的代码关联到按钮上，而做到这点的唯一办法就是为按钮创建一个Click事件处理程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904446529.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 51&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最后，在OnDetaching方法里解除它们之间的关联：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904457052.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 52&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，重新编译项目，然后从Assets面板上把AppBarButtonUpdateSource拖到Objects and Timeline面板的PhoneApplicationPage上，并把ButtonText属性的值设为"确定"。此时，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904451413.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 61&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;不过，这个顺序是不对的，AppBarButtonUpdateSource应该排在其它两个的前面，但这个顺序在Expression Blend里无法调整，因此我们需要手动修改XAML。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;至于取消按钮，由于它只是简单地返回，我们只需为它添置一个ApplicationBarButtonNavigation就行了。添置好后，Objects and Timeline面板将会变成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904468872.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 62&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，我们可以把这两个按钮的Click事件处理程序去掉了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;命令绑定是MVVM模式的重要组成部分，它不但可以进一步降低View和ViewModel之间的耦合度，还可以简化单元测试的工作。目前我们通过Behavior来实现命令绑定只是权宜之计，希望微软可以在将来的版本里把这部分功能补完了。还有的就是希望微软能够进一步完善Application Bar，包括处理焦点问题以及提供更丰富的访问方式。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;下课了&amp;#8230;&amp;#8230;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201101/201101111904477627.png" alt="" /&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/1933165.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2011/01/11/1933165.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html</id><title type="text">WP7有约（二）：课后作业</title><summary type="text">WP7有约（二）：课后作业Written by Allen LeeI'm tired of being what you want me to be, feeling so faithless, lost under the surface.– Linkin Park, Numb作业本 上节课布置的作业有做吗？没人吭声啊，看来大家都忘了哦，没事，我们这次弄个作业本出来，大家就有地方记作业了。...</summary><published>2010-12-14T00:19:00Z</published><updated>2010-12-14T00:19:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html"/><content type="html">&lt;p&gt;&lt;span style="color:#0070c0; font-size:20pt"&gt;&lt;strong&gt;WP7有约（二）：课后作业&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee&lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;em&gt;I'm tired of being what you want me to be, feeling so faithless, lost under the surface.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;&amp;#8211; Linkin Park, Numb&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;作业本&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/11/17/1879489.html"&gt;上节课&lt;/a&gt;布置的作业有做吗？没人吭声啊，看来大家都忘了哦，没事，我们这次弄个作业本出来，大家就有地方记作业了。在开始设计应用程序之前，我们先来看看通常的作业本是怎样记作业的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805476249.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图可以看到，作业本有点像日记本，每次记录时都会写下当天的日期，每天的作业又会根据课程进行归类。慢着！我怎么知道这些作业什么时候交？一般情况下，中小学生的作业都是第二天上课时交的，但大学生就不同了，他们的作业可能第二天交，也可能一周之后交，有时甚至几周之后才交，更重要的是，不同的作业可能在不同的时间交。换句话说，我们的应用程序还需要支持记录交作业的时间。此外，每当完成一项作业，我们可以在旁边做个记号，这样，当我们打开作业本时，即使作业再多也能马上知道哪些还没做完。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，用Visual Studio打开项目，在Models文件夹里创建一个Assignment类，和上节课的Course类一样，它也需要实现INotifyPropertyChanged接口。由于我们有很多类都需要实现INotifyPropertyChanged接口，为了避免不必要的重复，你可以考虑创建一个类专门实现这个接口，然后让有需要的类继承这个类。这个需求似乎比较常见，因此&lt;a href="http://compositewpf.codeplex.com/"&gt;Prism&lt;/a&gt;提供了一个NotificationObject类，我们只需继承它就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805473151.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;继承之前别忘了引用Bin\Phone\Microsoft.Practices.Prism.dll类库和Microsoft.Practices.Prism.ViewModel命名空间哦。根据前面的讨论，Assignment类应该包含以下属性：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;&lt;colgroup&gt;&lt;col style="width:197px"&gt;&lt;col style="width:197px"&gt;&lt;col style="width:197px"&gt;&lt;/colgroup&gt;&lt;tbody valign="top"&gt;&lt;tr style="background: #9bbb59"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性名字&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性类型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;备注&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Id&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Guid&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;唯一标识&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;CourseName&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;string&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;课程名称&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;StartDate&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;DateTime&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;创建日期&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;DueDate&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;DateTime&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;截止日期&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Content&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;string&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;作业内容&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;IsCompleted&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;bool&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;完成状态&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们知道，Id属性作为唯一标识，其值一旦生成就不会改变，因此我们只需在构造函数里初始化它就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805484754.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而其它属性则需要在它们的set访问器里调用从NotificationObject类继承过来的RaisePropertyChanged方法，比如说，我们可以这样实现IsCompleted属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805482770.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;看到这里，你可能会说，作业的状态应该不止"已完成"和"未完成"两种啊，比如说，当老师刚把作业布置下来时，它应该是"未开始"；当我们开始做某项作业时，它应该是"进行中"；有时候准备工作还没好，我们不得不把作业推迟，此时它应该是"已推迟"；有时候老师可能大发慈悲说某些作业不用做了，此时它应该是"已取消"，等等等等。照这样说，我们是否也该考虑把现在的两个日期细化为"计划开始日期"、"计划结束日期"、"实际开始日期"和"实际结束日期"，然后加上一个"作业进度"什么的？千万不要这样，没有学生愿意采用这么细致的作业管理方案，再说这样做也会分散他们的注意、加重他们的负担，&lt;strong&gt;作业本的主要目的只有一个，就是让学生对要做哪些作业一目了然&lt;/strong&gt;，所有功能的设计都应该围绕这点展开，所有功能的取舍也应该以此为标准。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;保存作业本&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;数据存储方面，我打算仿效课程表的做法，通过JSON序列化把作业本的数据保存到独立存储区，实现这个并不难，你可以照搬课程表的做法，创建一个IAssignmentStore接口和一个JsonAssignmentStore类。当你实现完JsonAssignmentStore类之后，你将会发现它和JsonCourseStore类有99.9%的代码是相同的，事实上，你可以把JsonCourseStore.cs文件复制一份，并重命名为JsonAssignmentStore.cs，然后把里面的"Course"字眼都替换成"Assignment"就可以了。不过，这种重复着实让人不爽啊，看来是时候重构一下了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;ICourseStore接口和IAssignmentStore接口的区别只在于集合元素的类型和集合属性的名字，前者可以通过泛型统一起来，至于后者，我们可以把属性的名字统一为Items，这样，两个接口就能统一起来了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805487197.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而实现方面，我们可以创建一个JsonDataStore&amp;lt;T&amp;gt;类，并让它实现IDataStore&amp;lt;T&amp;gt;接口：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805485527.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，之前我们把文件名硬编码在JsonXXXStore类里，那是因为它对于JsonXXXStore类来说是固定的、一对一的，而现在的JsonDataStore&amp;lt;T&amp;gt;类不再仅仅对应一个文件，因此我们把它保存在一个私有字段里。其它的和JsonAssignmentStore类没有太大出入。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;看到这里，有些同学可能会问，ICourseStore接口和JsonCourseStore类已经投入使用了，现在换用IDataStore&amp;lt;T&amp;gt;接口和JsonDataStore&amp;lt;T&amp;gt;类会不会造成很大影响？这个问题问得好，如果你确实不想修改其它代码，那你可以把JsonCourseStore类改造成JsonDataStore&amp;lt;Course&amp;gt;类的"马甲"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805492670.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，我们通过继承JsonDataStore&amp;lt;Course&amp;gt;类获得Rollback和Commit两个方法的实现，此外，由于其它代码是通过ICourseStore接口间接使用JsonCourseStore类的实例的，于是我们保留了ICourseStore接口，并把Courses属性重定向到Items属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;不过，就项目现在的规模而言，我们可以把重构做的更彻底一些，我们可以把ICourseStore.cs和JsonCourseStore.cs两个文件删除，如果你想保险一点，可以先把它们从项目排除出去，然后重新编译，此时Visual Studio会告诉你找不到ICourseStore接口和JsonCourseStore类，分别把它们替换成IDataStore&amp;lt;Course&amp;gt;接口和JsonDataStore&amp;lt;Course&amp;gt;类，调用后者的构造函数时记得提供文件名，即Courses.json，重新编译，此时Visual Studio会显示一堆错误，全部都是说找不到Courses属性的，把它们都替换成Items属性，重新编译，好了，如果那两个文件还没删除的话，现在可以安全删除了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;原型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在是时候考虑一下用户界面了，仔细观察我们的作业本（图1），是否觉得这种布局方式有种似曾相识的感觉？如果你一直关注WP7的相关消息，你可能已经看过类似的用户界面了&amp;#8212;&amp;#8212;People Hub的联系人列表。下面我们把它们两个放在一起看看：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805533113.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图可以看到，作业列表和联系人列表刚好能够对应起来，课程名称对应姓氏首字母，作为分组标题，而作业内容则对应联系人，作为分组内容。看到这里，你可能会问，WP7的Silverlight貌似没有这样的控件啊，难道要我们自己动手弄一个？原本是没有的，不过&lt;a href="http://silverlight.codeplex.com/releases/view/55034"&gt;十一月发布的SL for WP Toolkit&lt;/a&gt;已经增加了这个控件，名字叫做LongListSelector。上节课我们使用了Silverlight for Windows Phone Toolkit的TimePicker控件，当时引用的是九月份发布的版本，现在你可以下载新的版本，然后重新引用一下。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;仔细观察上图，你会发现作业列表上面有个日期没法对应到联系人列表，我们该怎么处理这个日期呢？这个问题问得好，事实上，这正是作业列表和联系人列表的最大区别，我们知道，联系人列表只有一份，但作业列表却会有很多份，每份都会有一个不同的日期，这些作业列表共同组成了一本作业本。如果把每份作业列表看作一个由标题和LongListSelector控件组成的页面，那么整个作业本就可以看作由N个这样的页面组成的应用程序了，但我们不必真的创建N个这样的页面，我们可以仿效课程表的做法，利用Pivot控件的特点，让每个Pivot项显示一份作业列表，这样Pivot项的标题可以用来显示作业列表上面的日期，而标题下面则通过LongListSelector控件显示每个课程的作业。不过，这样的设计是否真的妥当呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;试想一下，如果我们首先通过日期来划分Pivot项，接着通过课程来划分作业，那么每次我们要新建作业的时候，我们可能得先创建一个Pivot项，如果对应今天的Pivot项还没有的话，接着指定作业所属的课程，最后才填写和作业相关的信息，这个过程显然有点繁琐，我们应该尽可能简化其中的步骤。说到这里，有些同学可能会建议，不如让应用程序自动创建今天的Pivot项，这样至少可以省掉一个步骤。嗯，这个主意值得考虑，不过，并非每天都会有作业，比如说，今天是星期天，我进入作业本只是看一下这个周末有哪些作业，但应用程序却自动为我创建了今天的的Pivot项，而这并非我想要的，这意味着应用程序不得不在退出的时候把这个空的Pivot项删除。事实上，对于大学生来说，尤其是大三、大四的，今天有课明天没有是很常见的，难道要让用户设置哪天有课哪天没课，或者干脆直接解释课程表的数据，看看哪天有课哪天没课？从上面讨论不难看出，日期这个因素很不稳定，不太适合用来划分Pivot项，但课程就不同了，一旦课程表创建好了，作业本上会有哪些课程的作业也就定下来了，既然这样，何不把分组的顺序换一下？如果我们通过课程来划分Pivot项，那就不用考虑Pivot项的创建和删除了，因为用户在访问作业本的过程中会涉及到哪些课程是确定的，此外，当用户新建作业时也无需额外的步骤来指定今天的日期，因为这可以从DateTime的Today属性获取，这样我们就为用户省下两个步骤了。从这里我们可以看到，&lt;strong&gt;应用程序的设计绝对不是把控件堆砌起来显示数据就完事了，它包含的是一组完整的用户体验，而不同的组织方式可能会产生完全不一样的用户体验&lt;/strong&gt;，有时候多一两个步骤好像没什么大不了，但假如这一两个步骤要重复十次的话，用户就要额外执行十几二十个这样的步骤了，要么你为用户省下这些步骤，要么你让竞争对手为用户服务。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在让我们切换到Expression Blend，创建一个Windows Phone Pivot Page，并把它命名为AssignmentBookPage.xaml，完了之后把Pivot控件的Title属性设为"作业本"，把两个Pivot项的Header属性分别设为"数学"和"英语"，最后把一个LongListSelector控件拖到第一个Pivot项里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805533047.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接下来我们要为LongListSelector控件定制作业的显示方式，而执行这个任务的最佳场所是Expression Blend，但要发挥Expression Blend的潜能，我们需要准备一些示例数据，那么我们是否可以像上节课那样导入一些XML数据，然后把它们拖到LongListSelector控件上呢？很遗憾，不行，因为LongListSelector控件对于需要进行分组显示的数据源有特别要求。你可能以为我们只需把一个作业集合赋给ItemsSource属性，然后指定集合元素的某个属性作为分组依据，LongListSelector控件就会自动为我们分组，但事实并非如此，LongListSelector控件要求我们先把数据分组好，然后把这些分组凑成一个集合赋给ItemsSource属性，而且硬性规定每个分组至少实现IEnumerable接口，否则初始化时将会因为转换失败而抛出InvalidCastException异常，此外，为了便于显示分组标题，每个分组最好有个属性保存标题的内容，那么我们如何创建这样的数据源？其实创建这样的数据源并不难，LINQ的group XXX by YYY完全可以胜任这项任务，难处在于我们还想让它在Expression Blend的设计器上显示，所以我们得费一点儿周折了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，切换到Visual Studio，在ViewModels文件夹里创建一个AssignmentListViewModel类，并让它继承NotificationObject类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805547474.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，创建一个GetAssignments方法，返回一些Assignment对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805546569.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后，再创建一个AssignmentGroups属性，通过LINQ选取全部数学作业并根据创建日期进行分组：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805548488.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;做好这些准备工作之后，我们就可以着手把示例数据关联到用户界面上了。打开AssignmentBookPage.xaml文件，创建一个资源字典，并在里面创建一个AssignmentListViewModel对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805554867.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后就把第一个Pivot项的DataContext属性设为上面创建的AssignmentListViewModel对象，并把LongListSelector控件的ItemsSource属性绑到这个对象的AssignmentGroups属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805552326.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;此时，如果你切换到Expression Blend，它会提示你重新加载文件，因为刚才我们在Visual Studio里做了修改。加载完毕之后，你会看到LongListSelector控件里多了一些东西：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805569436.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图可以看出，示例数据已经绑上去了，但为什么显示出来的是"Iridescent.Models.Assignment"，而且每个都是一样？这是因为LongListSelector控件并不知道如何显示Assignment对象，所以直接调用它们的ToString方法获取可以显示的内容，而我们在创建Assignment类的时候并未重写ToString方法，所以LongListSelector控件调用的是从Object类继承下来的版本，这个版本返回的是对象的类型的完全限定名，也就是我们刚才看到的"Iridescent.Models.Assignment"。那么分组标题又哪去了？事实上，分组标题并未显示出来，因为LongListSelector控件并不知道分组的哪个属性表示分组标题。换句话说，LongListSelector控件压根不知道如何使用我们提供的数据，而把使用方法告诉它正是我们的责任。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;定制数据模板&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先是定制分组标题的数据模板，右击LongListSelector控件里的任何地方，选择Edit Additional Templates\Edit GroupHeaderTemplate\Create Empty：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805565815.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的Create DataTemplate Resource对话框里输入模板名字，然后按OK关闭对话框：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805575716.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;进入模板的编辑状态之后，你会看到一个空的Grid，从&lt;a href="http://msdn.microsoft.com/zh-cn/library/cc295260(v=Expression.40).aspx"&gt;Tools面板&lt;/a&gt;把一个TextBlock拖到Grid里，确保TextBlock处于选中状态（而不是编辑状态），单击Text属性右边的小正方形，并选择Data Binding：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805583141.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的Create Data Binding对话框里选中Use a custom path expression，并在旁边的编辑框里输入Key：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805582029.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;为什么输入Key呢？因为通过LINQ的group XXX by YYY创建的分组对象实现了IGrouping&amp;lt;TKey, TElement&amp;gt;接口，而这个接口有个Key属性保存了分组的依据&amp;#8212;&amp;#8212;创建日期，也就是这里需要的分组标题了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;当你按OK关闭对话框之后，你将会看到：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805588092.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;奇怪了！我们明明提供了示例数据啊，而且数据绑定也没弄错啊，为什么TextBlock没有任何显示？仔细观察Text属性下面的DataContext属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805588059.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时的值应该是分组对象而不是AssignmentListViewModel对象啊！我怀疑LongListSelector控件没有正确处理DataContext在设计时的传递（bug？），导致Expression Blend无法获取正确的数据。既然这样，我们只好再弄点示例数据了，单击Text属性右边的编辑框，选择Reset，然后把Text属性的值改为"2010/11/29"。接着，在Objects and Timeline面板上选中Grid，单击Background属性右边的小正方形，并选择System Resource\PhoneAccentBrush：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805598026.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，你的&lt;a href="http://msdn.microsoft.com/zh-cn/library/ee341448(v=Expression.40).aspx"&gt;Artboard&lt;/a&gt;应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140805593533.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;退出模板的编辑状态，保存所有修改，然后重新编译项目，好了之后就能看到分组标题了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806002354.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;不要奇怪分组标题都是"2010/11/29"，这是我们刚才为了编辑的方便硬编码上去的结果，暂时忍耐一下吧。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来是列表项的数据模板，右击LongListSelector控件里的任何地方，选择Edit Additional Templates\Edit ItemTemplate\Create Empty，在弹出的Create DataTemplate Resource对话框里输入模板名字（itemTemplate），然后按OK关闭对话框。现在，我们要思考的问题是，如何更好地显示作业数据呢？回顾表1，Id属性为了便于应用程序搜索Assignment对象而创建的，用户并不需要知晓它的存在，所以我们不必把它呈现在用户面前，Pivot项的标题已经显示了CourseName属性，分组标题也显示了StartDate属性，剩下的就是DueDate、Content和IsCompleted三个属性了，那么我们应该如何显示这三个属性？此时，我的脑子里浮现出的第一个想法是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080600369.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;整个Grid分为两个Column，左边是作业内容，自动换行，右边从上到下分别是截止日期的月、日和完成状态，一般情况下，创建日期和截止日期的年份都是一样的，所以我们没有必要提供重复的信息，即使碰到跨年的情况，用户也不会因为缺少年份而感到疑惑，除非有个老师布置了一个跨越两年或以上的作业。想到这里，我的脑子里突然闪出一个问题，表示完成状态的TextBlock能否去掉，并以其它方式表达这个信息呢？此时，我的脑子里迅速浮现出各种各样的图标，但是，还有更好的方式吗？颜色，突然这个词儿从我的脑子里掠过，一般而言，与文字相比，我们的大脑对颜色的反应更快更准。有鉴于此，我把列表项的模板改成这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806008384.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;右边部分将会根据作业的不同状态显示不同底色。退出模板的编辑状态，保存所有修改，然后重新编译项目，好了之后就能看到效果了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080602171.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;显然，字体的大小、控件之间的间距还不能让人满意，我们需要调整一下，这个过程可能有点反复和枯燥，但这却是我们体贴用户的重要途径，我们不但要让用户的眼睛感到满意，还要让用户的手指感到满意（别忘记我们开发的是触屏应用程序哦），下面是我调整之后的效果：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806043004.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，我们可以再次进入模板的编辑状态，为对应的控件设置数据绑定了，做法和前面为分组标题设置数据绑定的一样（图7和图8），各个控件对应的自定义路径表达式如下图所示：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806068936.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了之后就可以看到我们前面准备的示例数据了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806074136.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;噢，分组标题！我希望只显示日期，而且是符合中国区域设置的短日期格式，还有月份的显示，我希望是"十一月"而不是"11"。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;这个时候又轮到转换器出场了。首先，切换到Visual Studio，在Utils文件夹里创建下面两个类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806077134.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806082085.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，因为我们的绑定是单向的，所以没有必要实现ConvertBack方法。接着，在AssignmentBookPage.xaml的资源字典里创建它们的实例：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806083688.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，这两个转换器的Convert方法都使用了culture这个参数，但我们没有直接调用Convert方法啊，那我们怎么把这个参数传给它？这可以通过设置绑定表达式的ConverterCulture属性做到，现在，把那两个TextBlock的Text属性的绑定表达式改为"&lt;span style="color:blue"&gt;&lt;span style="font-family:Consolas"&gt;{&lt;span style="color:#a31515"&gt;Binding&lt;span style="color:red"&gt; Key&lt;span style="color:blue"&gt;,&lt;span style="color:red"&gt; Converter&lt;span style="color:blue"&gt;={&lt;span style="color:#a31515"&gt;StaticResource&lt;span style="color:red"&gt; dateConverter&lt;span style="color:blue"&gt;},&lt;span style="color:red"&gt; ConverterCulture&lt;span style="color:blue"&gt;=zh-CN}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;"&lt;/span&gt;和"&lt;span style="color:blue"&gt;&lt;span style="font-family:Consolas"&gt;{&lt;span style="color:#a31515"&gt;Binding&lt;span style="color:red"&gt; DueDate&lt;span style="color:blue"&gt;.Month,&lt;span style="color:red"&gt; Converter&lt;span style="color:blue"&gt;={&lt;span style="color:#a31515"&gt;StaticResource&lt;span style="color:red"&gt; monthNameConverter&lt;span style="color:blue"&gt;},&lt;span style="color:red"&gt; ConverterCulture&lt;span style="color:blue"&gt;=zh-CN}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;"&lt;/span&gt;。&lt;span style="color:blue; font-family:Consolas"&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;剩下的就是截止日期的底色了，既然转换器可以把DateTime对象转换成字符串，它也应该可以把Assignment对象转换成SolidColorBrush对象，不过，在创建这个转换器之前，我们得先弄清楚什么状态对应什么底色。前面我们说过，作业本的主要目的是让学生对要做哪些作业一目了然，而"未完成"的作业里可能存在一些已经过了截止日期的，这类作业需要马上处理，所以我们应该单独为这类作业设置一种底色，以便用户及时知晓并采取行动。假设这三种状态及其对应的底色如下表所示（你也可以换成其它底色）：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;&lt;colgroup&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;/colgroup&gt;&lt;tbody valign="top"&gt;&lt;tr style="background: #9bbb59"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;状态&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;底色&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;已逾期&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Red&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;未完成&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;#FF1BA1E2&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;已完成&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Green&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么转换器的Convert方法可以这样实现：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806088954.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在AssignmentBookPage.xaml的资源字典里创建它的实例（参考代码14），并把那个StackPanel的Background属性的绑定表达式改为"&lt;span style="color:blue"&gt;&lt;span style="font-family:Consolas"&gt;{&lt;span style="color:#a31515"&gt;Binding&lt;span style="color:red"&gt; Converter&lt;span style="color:blue"&gt;={&lt;span style="color:#a31515"&gt;StaticResource&lt;span style="color:red"&gt; assignmentToBrushConverter&lt;span style="color:blue"&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;"&lt;/span&gt;。&lt;span style="color:blue; font-family:Consolas"&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了之后就编译一下，没问题的话就可以看到效果了（你也可以在Visual Studio里看）：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806092443.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，"未完成"的底色和分组标题的底色是一样的，为什么不直接使用PhoneAccentBrush这个系统资源呢？这是因为用户有可能在手机的Settings里把Accent Color设成和其它状态一样的颜色，这会导致两种不同的状态应用相同的底色，而用户也有可能因此获得错误的信息。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，如果你试图编辑列表项的数据模板，你将会看到此番情景：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080610425.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;怎么回事？！从上图不难看出，底色转换器的Convert方法抛出一个InvalidCastException异常，而异常的信息也明确告诉我们无法把对象从AssignmentListViewModel类型转换成Assignment类型（把滚动条向右拖动就可以看到了）。在Convert方法里，我们只做过一次转换，就是在开始的时候把value参数转换成Assignment类型（参见代码15），因为此时的DataContext应该是Assignment对象，但上面这个异常却告诉我们value参数不是Assignment对象！为什么会这样？还记得前面编辑分组标题的数据模板时，即时我们设置好数据绑定也看不到示例数据（参见图9），当时我们猜测LongListSelector控件没有正确处理DataContext在设计时的传递（参见图10），而这个猜测在这里得到了证实。明白为什么会这样，问题就不难解决了，把Convert方法开始那行强制转换换成下面这段代码：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806105932.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;编译一下。现在，如果你尝试编辑列表项的数据模板，你不会再看到上面的异常了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来干嘛？你懂的！打开MainPage.xaml，添加一个菜单项，并让它导航至AssignmentBookPage页：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806119770.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，按F5吧&amp;#8230;&amp;#8230;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806123292.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;啊？！分组标题哪去啦？？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;插曲 #1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;究竟发生了什么事？示例数据和绑定表达式应该都没问题啊，否则Expression Blend和Visual Studio的设计器也不会正常显示，那么问题到底出在哪里呢？突然，一个想法在我的脑子里闪过，如果我在DateConverter类的Convert方法里设个断点，你觉得会怎么样？试一下吧&amp;#8230;&amp;#8230;结果是，没有到达这个断点，换句话说，Convert方法根本没被调用！这种情况有点像数据绑定找不到分组对象的Key属性，比如说，我故意把绑定表达式的Key改为Key1，结果Expression Blend的设计器就变成这样了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806135950.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们知道，分组对象实现了IGrouping&amp;lt;TKey, IElement&amp;gt;接口，因此Key属性肯定存在，否则编译器会报错，那么，什么情况下这个属性是不可见的，或者说，有什么办法可以让它不可见？想到这里，一个词儿突然在我的脑子里冒出来&amp;#8212;&amp;#8212;显式接口实现！如果Key属性是显式实现的，仅当变量的类型是IGrouping&amp;lt;TKey, IElement&amp;gt;时Key属性才是可见的。看到这里，你可能会说，Silverlight不可能直接调用分组对象的Key属性，它应该是通过反射获取这个属性的。没错，当我们在绑定表达式里以字符串的形式给出属性路径，PropertyPathConverter对象将会把这个字符串转换成PropertyPath对象，那么，PropertyPath对象又是如何找到对应的属性呢？在&lt;a href="http://referencesource.microsoft.com/netframework.aspx"&gt;微软公开的.NET Framework 4.0源代码&lt;/a&gt;里，我找到了PropertyPath类的实现，里面有个GetPropertyHelper方法负责获取指定的属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080614901.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;如果Key属性是显式实现的话，GetProperty方法就会返回null！换句话说，数据绑定和显式实现的属性一起工作的话会出问题。那么，group XXX by YYY返回的分组对象是不是显示实现Key属性的呢？我们知道，使用group XXX by YYY实质上就是调用Enumerable类的GroupBy方法，经过一番查找，我发现它返回的分组对象就是Lookup类内部的Grouping类的实例，但Grouping类的Key属性是隐式实现的，有趣的是，Key属性上方有一段注释：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080614868.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;除了Key属性之外，Grouping类的其它属性都是显式实现的，我猜Key属性原来也是显式实现的，后来由于数据绑定的问题才改为隐式实现。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;这些代码是WPF 4.0的，而Key属性上面的注释也明确提到了WPF，这是不是说Key属性的值在WPF里可以正确显示？我们可以设计一个简单的实验来验证一下：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;创建一个ListBox。&lt;/li&gt;&lt;li&gt;定制ListBox的ItemTemplate，里面只放一个TextBlock。&lt;/li&gt;&lt;li&gt;把TextBlock的Text属性设为"{Binding Key}"。&lt;/li&gt;&lt;li&gt;通过GroupBy方法创建分组对象的集合，并把它绑到ListBox的ItemsSource属性。&lt;/li&gt;&lt;li&gt;按F5。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;我分别在WPF 4.0、SL 4.0和SL for WP7上执行这个实验，发现只有WPF 4.0能够正确显示Key属性的值，其它两个的ListBox是一片空白的。我怀疑SL的分支是在这个问题得到修复之前创建的，但我没有代码证实这个猜想。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;还有一个问题我没弄明白的，为什么设计器能够正确显示而程序真正运行的时候却不能？难道设计器对显式实现的属性有什么特别的照顾？为了验证这个猜想，我又做了一个实验，我不直接返回分组对象，而是通过下面这个Grouping类包装一下再返回：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806146375.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;结果，设计器也不显示了&amp;#8230;&amp;#8230;我不知道为什么设计器能够正确显示GroupBy方法返回的分组对象的Key属性，这里面肯定有些东西是我不知道的，如果你知道原因，或者先我一步找到原因，那你一定要告诉我哦！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;连接前端和后端&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;既然显式实现的属性会对数据绑定造成不良影响，那我们就换成隐式实现吧。首先，在ViewModels文件夹里创建AssignmentGroupViewModel类，并让它继承ObservableCollection&amp;lt;Assignment&amp;gt;类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080615802.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;为什么要继承ObservableCollection&amp;lt;Assignment&amp;gt;类呢？前面说过，LongListSelector控件硬性规定分组对象至少实现IEnumerable接口，不过，要想获得更好的效果，仅仅实现IEnumerable接口是不够的，LongListSelector控件通过内部的GetItemsInGroup方法来获取分组内容：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806153244.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上面代码不难看出，如果分组对象实现了IList接口，那么每次获取分组内容时都会免掉一次遍历。此外，我们还希望当分组内容发生改变时，比如新建/删除一项作业，分组对象能够自动通知LongListSelector控件做出相应的更新，为了实现这个效果，分组对象需要实现INotifyCollectionChanged接口。毫无疑问，能够一次过满足我们所有要求的最简单做法就是继承ObservableCollection&amp;lt;Assignment&amp;gt;类了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;看到这里，你可能会问，IGrouping&amp;lt;TKey, TElement&amp;gt;接口不用实现吗？不用，LongListSelector控件没有规定分组对象必须实现这个接口，我们只需简单地创建一个Key属性，配合绑定表达式里的属性路径就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806162339.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，ObservableCollection&amp;lt;Assignment&amp;gt;类也实现了INotifyPropertyChanged接口，所以我们可以直接使用它的OnPropertyChanged方法。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来是分组对象的初始化，这个过程的主要任务有两个：&lt;/p&gt;&lt;ol&gt;&lt;li&gt;查询数据源，把满足条件的作业内容添加到自身。&lt;/li&gt;&lt;li&gt;监听数据源，把满足条件的内容更改反映到自身。&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;执行这两个任务的前提是有个可用的数据源，我们可以仿效课程表的做法，在App类里通过静态属性提供JsonDataStore&amp;lt;Assignment&amp;gt;对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806161750.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;有了数据源我们就可以着手执行第一个任务了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806165305.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，这里把判断条件单独提取出来了，因为执行第二个任务时还要用到：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080617255.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，e参数的NewItems和OldItems两个属性看起来好像可能包含多个元素，但事实上它们只会包含一个，因为NotifyCollectionChangedEventArgs类的构造函数限制了这个可能，不过这个限制仅存在于Silverlight的现有版本（SL3、SL4、SL for WP7）。另外，这里使用了Lambda语句来创建CollectionChanged事件的处理程序，虽然你也可以通过一个单独的方法做到，但使用Lambda语句可以利用闭包的特点重用前面的判断条件，当然，使用匿名方法的语法也是可以的。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;还差什么呢？噢，对了，LongListSelector控件内部会调用分组对象的Equals方法进行判等，我们可以重写AssignmentGroupViewModel类的Equals和GetHashCode两个方法，使之根据Key属性来判等以及获取哈希值。这个任务留给你当课后作业吧。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;既然分组对象的类型改了，那AssignmentListViewModel类的AssignmentGroups属性也得做出相应的调整吧：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806173046.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;由于AssignmentListViewModel类对应用户界面上的Pivot项，我们还需要给它创建一个Title属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806172697.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;有了这些准备，我们就可以着手实现AssignmentListViewModel类的构造函数了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806184060.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会说，这条LINQ语句看起来有点复杂嘛！其实不然，想想看，我们的最终目的是什么？创建分组对象并把它们添加到AssignmentGroups属性。那创建分组对象需要什么条件？课程名称和创建日期。课程名称已经有了，创建日期来自哪里？来自数据源。那我们对创建日期有些什么要求？我们只要和指定课程相关的，而且不要重复的。现在，你再看看上面这条LINQ语句，从上往下看，有没有觉得它像下面这条"流水线"？&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806196403.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;前面我们说过，当用户新建一项作业时，它会自动添加到"今天"的分组里，但如果"今天"的分组还没创建出来呢？那AssignmentListViewModel类就应该为这项新的作业创建"今天"的分组，并把它添加到AssignmentGroups属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806201353.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当用户删除一项作业时，如果这项作业是所属分组的唯一一项作业，LongListSelector控件会自动隐藏这个分组。而当用户撤销所有更改时，AssignmentListViewModel类得把AssignmentGroups属性清空。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;到目前为止，AssignmentBookPage页里的每个组成部分都有对应的ViewModel类了，现在是时候为它创建一个了。在ViewModels文件夹里创建一个AssignmentBookViewModel类，并创建一个AssignmentLists属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806203272.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;AssignmentBookViewModel类的任务是读取课程表的数据，然后创建对应的AssignmentListViewModel对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080620731.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，为什么这里不用监听数据源的更改？如果你要编辑课程表，一定要进入课程表的用户界面，一旦离开课程表的用户界面，课程表的数据就会冻结下来，换句话说，在AssignmentBookViewModel对象的整个生命周期里，课程表的数据是稳定的。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，我们可以着手处理数据绑定了。打开AssignmentBookPage.xaml文件，切换到XAML模式，在页面的资源字典里添加两个数据模板：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806217318.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，把现有的Pivot项删除，并在Pivot控件上设置数据模板和数据绑定：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806213697.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最后在AssignmentBookPage的构造函数里创建一个AssignmentBookViewModel对象，并它把赋给DataContext属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/2010121408062175.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了，不知不觉又到看效果的时候了！按F5运行应用程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080622566.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击"课程表"菜单项进入课程表，新建两个课程，保存，然后按Back键返回主菜单：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806221056.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在主菜单里单击"作业本"菜单项进入作业本，此时，你会看到作业本已经为刚才创建的两个课程准备了两个Pivot项：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806236563.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;只是作业本上没有任何内容，也没有任何途径可以添加内容&amp;#8230;&amp;#8230;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;编辑作业本&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;作业本支持的操作和课程表一样，包括新建、编辑、删除、保存所有更改和撤销所有更改，其中，新建和保存以ApplicationBarIconButton的方式放在Application Bar上，撤销所有更改以ApplicationBarMenuItem的方式放在Application Bar上，而编辑和删除则放在上下文菜单里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806242004.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;为什么这样安排？当老师布置作业时，我们会掏出作业本记下作业，下课之后，当我们要做作业时，我们会掏出作业本看看要做哪些作业，换句话说，新建、保存和显示作业内容这三个功能已经可以满足用户绝大多数的需求了。新建和保存作为最常用的两个操作自然应该放在最显眼的位置，删除和撤销所有更改这两个操作基本上不会用到，至于编辑，一般情况下我们只是用来修改作业的完成状态，由于编辑和删除是针对特定作业的，我们把它们放在上下文菜单里，当用户长按某项作业时将会显示出来，而撤销所有更改则隐藏在Application Bar的菜单里。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接着，创建一个Windows Phone Page，并把它命名为NewOrEditAssignmentPage.xaml，这个页面会在用户单击Application Bar上的新建按钮或者上下文菜单上的编辑菜单项时显示。完了之后把ApplicationTitle的Text属性值改为"作业本"，但PageTitle保留原样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806246398.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，这个页面应该放些什么控件呢？想想看，创建一个完整的Assignment对象需要哪些数据？Id是自动生成的，课程名称可以从上下文获取，创建日期可以从DateTime的Today属性获取，剩下的就是截止日期、作业内容和完成状态了。截止日期可以使用SL for WP Toolkit的DatePicker控件，作业内容可以使用TextBox控件（上面的标题需要额外添置TextBlock控件），而完成状态则可以使用CheckBox控件：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806251872.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，为什么不把其它信息也显示出来呢？你可以这样做，但是，请注意，这个页面的主要目的是收集而不是显示信息，我们应该尽可能简化用户的输入过程，在这里放置控件显示其它信息，尤其是可编辑的控件，可能会耗费用户额外的注意力，比如说，有些用户会下意识地检查所有数据是否输入正确。创建作业的过程应该是既简单又快速的，而我们也希望用户能有这样的感受，但耗费用户额外的注意力意味着增加整个操作过程的时间，从而可能导致用户的感受和我们期望的刚好相反，这是我们不希望看到的。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;ViewModel类方面，我们将会仿效课程表的做法，创建NewOrEditAssignmentViewModel、NewAssignmentViewModel和EditAssignmentViewModel三个类：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806261773.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们知道，NewOrEditAssignmentPage页有两个模式，一个是新建模式，另一个是编辑模式，前者对应NewAssignmentViewModel类，而后者则对应EditAssignmentViewModel类。当用户新建一项作业时，NewAssignmentViewModel类可以从DateTime的Today属性获取创建日期，但它没法获取课程名称，所以我们需要通过参数传给它：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806269231.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;为什么DueDate属性也要设置呢？想想看，如果我们不给它设置一个值，由于DateTime是值类型，将被自动初始化为"1/1/0001"，当用户看到页面上的DatePicker控件显示这样一个日期可能会感到不友好，再者，老师布置下来的作业一般不会当天交（课堂作业除外），而第二天交的情况则比较常见（当然，计算下一个"上课日"可能更加合理）。而当用户编辑一项作业时，EditAssignmentViewModel类将会从数据源里查找这项作业的数据，但前提是我们把作业的Id告诉它：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806278642.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，Assignment类的Id属性是只读的，而Assignment类原来的构造函数会在每次调用时创建一个新的Id，这导致了我们无法使用现有的Id，所以我们需要在Assignment类里添加下面这个构造函数：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806273069.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;创建好ViewModel类之后，我们就可以着手处理它们和NewOrEditAssignmentPage页之间的关联了。首先是设置数据绑定，需要设置的控件以及对应的绑定表达式如下表所示：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;&lt;colgroup&gt;&lt;col style="width:73px"&gt;&lt;col style="width:79px"&gt;&lt;col style="width:77px"&gt;&lt;col style="width:324px"&gt;&lt;/colgroup&gt;&lt;tbody valign="top"&gt;&lt;tr style="background: #9bbb59"&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;描述&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;类型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;绑定表达式&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;页面标题&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;TextBlock&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Text&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding Title}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;截止日期&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;DatePicker&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Value&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding Assignment.DueDate, Mode=TwoWay}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;作业内容&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;TextBox&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;Text&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-top:  none; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding Assignment.Content, Mode=TwoWay}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-left:  solid #9bbb59 1.0pt; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;完成状态&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;CheckBox&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;IsChecked&lt;/p&gt;&lt;/td&gt;&lt;td style="padding-left: 7px; padding-right: 7px; border-bottom:  solid #9bbb59 1.0pt; border-right:  solid #9bbb59 1.0pt"&gt;&lt;p&gt;{Binding Assignment.IsCompleted, Mode=TwoWay}&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，重写OnNavigatedTo方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806281923.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最后，为两个按钮创建事件处理程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806289382.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;由于这部分内容和上节课的大同小异，这里就不详细解释了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来是实现前面提到的五个操作。首先是最常用的新建和保存。保存操作非常简单，我们只需为它创建一个事件处理程序就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806289033.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;而新建则有点难度，我们需要获取课程名称，怎么获取？我们知道，课程名称实际上就是Pivot项的标题，也就是AssignmentListViewModel的Title属性，只要我们知道当前显示的是哪个AssignmentListViewModel对象就可以了。为此，我们需要在AssignmentBookViewModel类里添加一个SelectedListIndex属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806293460.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;并为它和Pivot控件的SelectedIndex属性设置双向绑定。此外，需要说明的是，为了使用RaisePropertyChanged方法，我们需要让AssignmentBookViewModel类继承NotificationObject类。有了这些准备，我们就可以创建事件处理程序了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806295903.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 42&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，如果课程表里面没有课程，Pivot控件就不会创建Pivot项，所以在做进一步处理之前，我们需要判断AssignmentLists里面有没有东西。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了，又到看效果的时候了！按F5运行应用程序，在主菜单里单击"作业本"菜单项进入作业本：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806309217.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击Application Bar上的新建按钮创建一项作业：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806311277.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击确定返回：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080631688.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Oh，My Lady Gaga！我的作业呢？？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;插曲 #2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;究竟发生了什么事？是数据没有添加进去？是事件通知没有发出？还是出现线程安全的问题？我调试了一下，数据已经正确添加进去了，事件通知也正确发出去了，所有操作都在UI线程里执行，而且没有出现并发问题，那么问题到底出在哪里呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;带着这个疑问，我从codeplex.com上下载了&lt;a href="http://silverlight.codeplex.com/SourceControl/changeset/changes/57505"&gt;SL for WP Toolkit的最新代码（Change Set 57505）&lt;/a&gt;，然后调试进去看看。在调试的过程中，我发现每次从NewOrEditAssignmentPage页返回AssignmentBookPage页时，LongListSelector控件都会调用Balance方法，但每次都会"跳过"本应执行的大部分代码，一开始我没怎么留意，觉得这个方法一下子就返回实在太神奇了，仔细观察，原来它是通过第一个if里的return悄悄返回的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806314243.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 43&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;难怪LongListSelector控件什么也没显示，因为Balance方法后面那些负责调整显示的代码一句都没执行。为什么会这样？关键在于IsReady方法，因为它每次都返回false。当我单步进入IsReady方法时，发现_itemsPanel和ItemsSource都不为null，但ActualHeight的值却为0.0，从而导致IsReady方法返回false：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806324210.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 44&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;为什么会这样？这是因为，当我们打开NewOrEditAssignmentPage页时，由于AssignmentBookPage页暂时无需显示，Silverlight会把它从主对象树移除，于是ActualHeight会被"清零"，当我们从NewOrEditAssignmentPage页返回时，Silverlight需要重新测量每个控件的大小（包括页面本身），并安排它们的位置，ActualHeight的值为0.0意味着Silverlight还没完成布局处理的工作，换句话说，LongListSelector控件还没准备好，IsReady方法返回false是正确的。奇怪的是，每次我们从NewOrEditAssignmentPage页返回时，Balance方法里的IsReady方法没有一次返回true的，这可能意味着Balance方法的调用时机不对，那什么时候调用才对呢？控件加载完毕的时候，即Loaded事件触发的时候，那么，LongListSelector控件在Loaded事件触发的时候做了些啥呢？其实没什么，只是简单地把_isLoaded设为true，然后调用EnsureData方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080632589.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 45&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这么看来，问题的关键就在于EnsureData方法有没有正确调用Balance方法了。我们来看看EnsureData方法的代码：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806320.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 46&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;FlattenData和Balance是两个很重要的方法，前者负责从ItemsSource把数据初始化到_flattenedItems，而后者则负责确定哪些数据需要显示以及如何显示。显然，当我们从NewOrEditAssignmentPage页返回时，如果我们创建了作业，if里面的语句是不可能执行的，因为_flattenedItems里面包含了我们的作业！？这听起来很别扭，不是吗？毫无疑问，LongListSelector控件没有考虑我们的情况，即打开一个另一个页面操作数据源，这是不应该的，你不可能指望我们把所有事情都放在同一个页面里处理吧？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;既然知道了原因，问题就不难解决了，把LongListSelector控件的Loaded事件处理程序改成下面这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806337458.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 47&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，_isLoadedRaisedBefore是干嘛的？我们知道，第一次进入AssignmentBookPage页和从NewOrEditAssignmentPage页返回时都会触发Loaded事件，这是两种需要区别处理的情况，因为Balance方法里包含了重设_resolvedFirstIndex和_resolvedCount的代码（参见代码43），如果我们在后面那种情况下执行这行代码，LongListSelector控件的显示就会乱掉，因为它计算不出正确的显示索引，_isLoadedRaisedBefore的存在就是为了防止这种情况的发生。接着，在Balance方法里用if把重设_resolvedFirstIndex和_resolvedCount的那行代码包围起来：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806338505.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 48&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得提醒的是，每次调用FlattenData方法都会重设_flattenedItems，这对于从NewOrEditAssignmentPage页返回的情况来说是没有必要的，所以Loaded事件处理程序里的FlattenData方法需要放在if里，否则，使用ObservableCollection就会变得毫无意义了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;改好之后，编译一下。注意，如果你是通过MSI安装SL for WP7 Toolkit的话，你需要先在项目属性里修改一下版本再编译，否则待会重新添加引用的时候Visual Studio会自作聪明的引用原来那个dll文件，因为MSI&lt;a href="http://msdn.microsoft.com/zh-cn/library/1h52t681.aspx"&gt;在注册表里做了手脚&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;一切准备就绪之后就可以按F5了。单击Application Bar上的新建按钮打开NewOrEditAssignmentPage页：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806343456.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;输入作业内容，然后按确定返回：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806354229.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;噢，终于看到我的作业啦！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;编辑作业本&amp;#183;续&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;回到作业本的操作，接下来我们要实现编辑和删除两个操作。前面提到，我打算把它们放在上下文菜单里，那么，如何创建上下文菜单？非常简单，我们可以使用SL for WP Toolkit的ContextMenu控件：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806353323.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 49&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;正如你所看到的，ContextMenu控件只需嵌入目标对象就能工作了，非常方便。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来的问题是如何实现它们的事件处理程序。我们知道，这两个操作有一个共同点，就是要获取用户当前选中的作业，怎么获取呢？有些同学可能会建议，在AssignmentListViewModel类里添加一个SelectedAssignment属性，并为它和LongListSelector控件的SelectedItem属性设置双向绑定，这样，一旦用户选中某项作业，我们就可以通过SelectedAssignment属性获取作业的Id了。你可以这样做，不过，这个做法会带来一个小小的问题，就是用户在长按某项作业之前得先单击一下。什么意思？我们知道，手机没有鼠标右击的概念，我们是通过长按（Touch and Hold）打开上下文菜单的，但从触摸手势的角度来看，长按和单击（Tap）是两个不同的触摸手势。LonglistSelector控件只会在单击的时候设置SelectedItem属性，它不处理长按，所以当我们通过长按打开上下文菜单时，SelectedItem属性可能为null或者之前选中的其它作业，前者会引发异常，而后者则会为用户带来困扰。为了避免这些问题，要么我们再次修改LongListSelector控件的代码，要么用户不得不执行一步额外的操作，显然，这都不是什么好办法，还有没有别的选择？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;当然有！你知道吗，DataContext属性是一个很特别的属性，子元素可以从父元素那里继承这个属性的值，对照代码49来看，这意味着MenuItem的DataContext和Grid的有着相同的值，而这个值正是我们苦苦寻找的作业！换句话说，只要我们获取到用户单击的MenuItem对象，就可以通过它的DataContext属性获取用户想要操作的作业。我们知道，事件处理程序的第一个参数就是引发该事件的对象，于是我们可以通过这个参数来访问MenuItem对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806367717.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 50&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，我们既不需要在AssignmentListViewModel类里添加一个SelectedAssignment属性，也不需要修改LongListSelector控件的代码，更不需要委屈用户执行额外的操作，真是一举三得啊！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在只剩一个操作了&amp;#8212;&amp;#8212;撤销所有更改，我相信这对于你来说不是问题，所以我决定把它留给你当课后作业。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了，又到看效果的时候了！按F5运行应用程序，新建三项作业：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806367651.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;长按第三项作业，你会看到这项作业以外的所有东西都缩小了，给人一种向后移动的感觉，这个动画生动地突出了正在操作的作业以及上下文菜单，不过，不知道是不是动画的bug，第三项作业的截止日期上面有个瑕疵（试了几次都是这样）：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806371489.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击编辑将会打开NewOrEditAssignmentPage页，修改一下截止日期：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806388566.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后按确定返回，你会看到刚才修改的截止日期：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806394356.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，长按第二项作业（Textbook. P20. Ex 2），并选择删除：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806396798.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 42&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;作业成功删除。但是，如果你尝试删除（剩下的）第二项作业，你会发现它还在那里！为什么！？我调试了一下，发现此时MenuItem对象的DataContext属性的值居然是已故的前任第二项（Textbook. P20. Ex 2），而不是我们期望的现任第二项（Textbook. P21. Ex 3）！因为前任第二项已被删除，所以Remove方法不会触发CollectionChanged事件，LongListSelector控件自然不会更新显示。如果你现在尝试删除第一项作业（既是前任也是现任），你会成功的，但是，在删除之后，如果你再次尝试删除剩下的唯一一项作业，你会发现此时MenuItem对象的DataContext属性的值变成刚故的前任第一项（Textbook. P10. Ex 9, 10）！从此以后，剩下的唯一一项作业就再也删除不了了，除非你返回MainPage页重新打开AssignmentBookPage页。由于编辑操作采用了相同的实现思路，如果一项作业删除不了，那么它也编辑不了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;究竟发生了什么事？是ContextMenu控件的bug吗？我另外创建了一个新的项目，在同等条件下，分别在ListBox和LongListSelector上测试了ContextMenu，结果，Listbox一方表现正常，而LongListSelector一方问题依旧，有趣的是，即使不用打开新的页面，结果还是一样。这让我不得不再一次怀疑是LongListSelector控件的问题。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我重新运行应用程序，然后单步执行第一次删除的整个过程。在这个过程里，我发现一个很奇怪的事情，当我删除第二项时，LongListSelector控件先把第三项的ContentPresenter和Assignment分离开来，并把分离出来的ContentPresenter推入内部的_recycledItems（类型为Stack&amp;lt;ContentPresenter&amp;gt;），接着对第二项做相同的事，然后把第二项从_flattenedItems里删除，最后重新关联第三项的ContentPresenter和Assignment，问题就出现在最后一步，它居然直接使用_recycledItems顶部的ContentPresenter，换句话说，它把第二项的ContentPresenter和第三项的Assignment关联了！见鬼！此时，如果我删除第一项的话，它会把第一项的ContentPresenter和第三项的Assignment关联！从这里不难看出，它应该在关联之前把_recycledItems顶部那个垃圾扔掉！既然知道了原因，问题就不难解决了，在OnRemove方法的相应地方加上红框里面那句：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/201012140806398401.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 51&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;重新编译所有东西，然后运行应用程序，这次没问题了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;写完这篇文章之后，我的第一感觉是LongListSelector控件远未达到产品级别的质量，它的问题导致我无法专注于应用程序本身的功能设计和实现，如果你是本着学习和研究的态度去用它，那没问题，如果你想用它来做产品，那你就要做好心理准备了。不管怎样，这次我还是学到了不少东西。LongListSelector控件的补丁我已经提交到codeplex.com了，在官方发布修正版本之前，我只能使用自己修改的版本了&lt;span style="font-family:微软雅黑"&gt;&amp;#8594;_&amp;#8594;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;下课了&amp;#8230;&amp;#8230;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/201012/20101214080641744.png" alt="" /&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/1905184.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/12/14/1905184.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2010/11/17/1879489.html</id><title type="text">WP7有约（一）：课程安排</title><summary type="text">WP7有约（一）：课程安排Written by Allen LeeAll I want to do is trade this life for something new holding on to what I haven't got.– Linkin Park, Waiting For The End你好，老七！ WP7终于发布了，到目前为止，有关它的新闻和介绍我相信你已经看过不少了，所以...</summary><published>2010-11-17T00:22:00Z</published><updated>2010-11-17T00:22:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2010/11/17/1879489.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2010/11/17/1879489.html"/><content type="html">&lt;p&gt;&lt;span style="color:#0070c0; font-size:20pt"&gt;&lt;strong&gt;WP7有约（一）：课程安排&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;&lt;em&gt;Written by Allen Lee&lt;/em&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;em&gt;All I want to do is trade this life for something new holding on to what I haven't got.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;&amp;#8211; Linkin Park, Waiting For The End&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;你好，老七！&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;a href="http://www.microsoft.com/windowsphone/en-us/default.aspx"&gt;WP7&lt;/a&gt;终于发布了，到目前为止，有关它的新闻和介绍我相信你已经看过不少了，所以这里将会直接跳过，不过在开始之前，我认为还是有必要提醒你做好相关的准备：&lt;/p&gt;&lt;ul&gt;     &lt;li&gt;&lt;a href="http://download.microsoft.com/download/1/7/7/177D6AF8-17FA-40E7-AB53-00B7CED31729/vm_web.exe"&gt;Expression Blend 4 for Windows Phone和Visual Studio 2010 Express for Windows Phone&lt;/a&gt;，你并不需要完整的Expression Studio 4 Ultimate和Visual Studio 2010 Ultimate，不过如果你有的话*可能*会更好。     &lt;/li&gt;     &lt;li&gt;白开水，大量白开水，接下来你将会与我一起进行大量脑力活动，你需要补充足够的水分才能让大脑更好地工作。     &lt;/li&gt;     &lt;li&gt;零食，最好是坚果类，薯片也可以，人无法长时间集中精力，也不该迫使自己长时间集中精力，当你感到注意力开始涣散时，不妨抓一把零食放到嘴里嚼，注意别弄到键盘上哦。     &lt;/li&gt;     &lt;li&gt;最后，也是最重要的，你，没错，是你，仅当你准备好接受新的知识时，你的大脑才会对它们进行积极的处理，否则就会把它们挡在外面。     &lt;/li&gt;&lt;/ul&gt;&lt;p&gt;那么，你准备好了吗？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，打开Expression Blend，创建一个Windows Phone Panorama Application项目：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP71.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;项目创建好之后，你会看到一个充满整个页面的Panorama控件，里面有两个Panorama项，每个Panorama项里面有一个ListBox，而ListBox里也有了示例数据。你可以调整&lt;a href="http://msdn.microsoft.com/en-us/library/ee341448(v=Expression.40).aspx"&gt;Artboard&lt;/a&gt;的缩放比例，以便显示整个UI：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP72.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;注意，这里所说的整个UI是指手机屏幕所能显示的部分，而Panorama控件具有延伸到屏幕以外区域的特性，所以我们无法一次过把整个Panorama控件尽收眼底，这确实是一件憾事。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接着，我们来看看Panorama控件，如果你对它的效果没有感性认识，不妨到先看看&lt;a href="http://www.microsoft.com/windowsphone/en-us/features/default.aspx"&gt;WP7的6个内置Hub&lt;/a&gt;。认识Panorama控件的最简单方法是结合&lt;a href="http://msdn.microsoft.com/en-us/library/cc295113(v=Expression.40).aspx"&gt;Objects and Timeline面板&lt;/a&gt;和Artboard来体验一下：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP73.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;如上图所示，每个Panorama控件都是由一个标题和若干Panorama项构成的，而每个Panorama项又会包含一个标题和一些内容，在这里，这些内容是通过ListBox来展示的，你可以根据实际的需要把它换成任何其它控件。此外，需要说明的是，Panorama控件和Panorama项的标题都已经内化成自身的属性，只需通过&lt;a href="http://msdn.microsoft.com/en-us/library/cc294866(v=Expression.40).aspx"&gt;Properties面板&lt;/a&gt;设置就可以了，无需额外添加TextBlock或者其它控件。现在，我们的Panorama控件包含了两个Panorama项，但从上图可以看到，只有第一个能完全显示出来（由于截图的关系，Artboard的一部分隐藏在滚动条下面），而第二个只能看到一小部分，那么，如何才能显示第二个Panorama项，以便操作上面的控件呢？答案非常简单，只需在Objects and Timeline面板上单击第二个Panorama项就可以了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP74.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得提醒的是，为了在操作时不影响其它Panorama项，我们还可以通过Objects and Timeline面板把其它Panorama项锁定，正如上图所示的那样。在继续阅读下面的内容之前，我强烈建议你稍稍暂停一下，把注意力集中在Objects and Timeline面板上，熟悉一下各个对象之间的关系，试着单击每个对象，然后看看它对应了Artboard上的哪个对象。如果你已经迫不及待想要亲自体验一下Panorama控件的效果，你现在可以按F5了。&lt;/p&gt;&lt;p&gt; 接下来，我们要执行以下任务：&lt;/p&gt;&lt;ul&gt;     &lt;li&gt;修改Panorama控件的标题     &lt;/li&gt;     &lt;li&gt;去掉Panorama控件的背景     &lt;/li&gt;     &lt;li&gt;删除现有的两个Panorama项     &lt;/li&gt;     &lt;li&gt;添加一个新的Panorama项     &lt;/li&gt;&lt;/ul&gt;&lt;p&gt;第一个任务非常简单，确保Objects and Timeline面板上的Panorama控件处于选中状态，在Properties面板上的搜索框里输入Title，第一个搜索结果就是我们要找的属性了，修改这个属性的值，然后按回车：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP75.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;第二个任务也挺简单，在Properties面板上的搜索框里输入Back，然后选择No brush就可以了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP76.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;第三个任务更简单，按下Ctrl键，依次选中两个Panorama项，然后按Del键就可以了。最后一个任务是添加新的Panorama项，打开&lt;a href="http://msdn.microsoft.com/en-us/library/cc294939(v=Expression.40).aspx"&gt;Assets面板&lt;/a&gt;，在搜索框里输入Pan：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP77.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后把PanoramaItem拖到Panorama控件上就可以了。注意，你可以把PanoramaItem拖到Objects and Timeline面板的Panorama控件上，也可以拖到Artboard的Panorama控件上，如果Artboard上的控件比较多，并且把Panorama控件挡住了，那么当你把PanoramaItem拖到Artboard上时，有可能会把它误加到其它控件上。这是添加控件的一般方法，针对添加PanoramaItem，我们还有更简单的方法，那就是右击Panorama控件，然后选择Add PanoramaItem就可以了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP78.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，向Panorama项添加一个TextBlock，内容随你，调整一下位置和大小，然后按F5：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP79.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;一般地，Panorama控件至少包含两个Panorama项，而这里只有一个，属于边界情况，细心观察上图，表面上，右边好像还有一个Panorama项，但当你在屏幕上向左滑动时，你会发现这其实是同一个Panorama项。那么向右滑动呢？情况一样。利用这个特点，我们可以创建一个简易计数器，把Panorama项的TextBlock绑定到一个计数变量上，当我们向左滑动时，计数变量加1，向右时则减1，其效果就像我们拥有一个无限延伸的Panorama控件，而边界情况就是这个计数变量的最大值和最小值，尽管如此，我们也无需太过担心，假设计算变量的类型是Int32，我相信没有人会向左或者向右滑动超过20亿次吧？如果你有兴趣的话，不妨把它当做课后练习。现在，按Back退出应用程序。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;上课啦！&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;上课啦？什么课？哪里上？看到这些问题，有没有一种亲切的感觉？说不定你今天就问了这些问题哦，那时你是不是在找课程表呢？如果课程表就在手机里该多好啊！事不宜迟了，我们自己弄一个吧。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;右击&lt;a href="http://msdn.microsoft.com/en-us/library/cc294752(v=Expression.40).aspx"&gt;Projects面板&lt;/a&gt;里的项目节点，选择Add New Item：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP710.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的New Item对话框里选择Windows Phone Pivot Page，输入页面的名字，然后按OK：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP711.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;和Panorama页一样，Pivot页也有一个充满整个页面的Pivot控件，刚创建好的Pivot控件默认附带两个Pivot项，我们可以把它们分别用于星期一和星期二。确保Pivot控件处于选中状态，在Properties面板上寻找Title属性，并把它的值改为"课程表"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP712.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着在Objects and Timeline面板上选中第一个Pivot项，在Properties面板上寻找Header属性，并把它的值改为"星期一"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP713.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;完了之后把第二个Pivot项的Header属性值改为"星期二"。此时，你的 Pivot控件应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP714.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;嗯，看起来像个样了，然而，标题下面那么大的一块空位应该怎么处理呢？毫无疑问，以列表的方式呈现一天的课程是比较适合的，但是，我希望列表的每一项除了显示课程名称之外，还能显示上课时间和上课地点。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;在继续设计UI之前，我们需要导入一些示例数据，以便在设计时就能看到最终效果。当然，你也可以让Expression Blend为你生成这些数据，不过，它无法为我们生成课程名称以及适合的上课地点，这样，当你在设计时调整控件外观时就会感到缺了点儿什么，而这正是使用真实数据的好处。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;假设我们要导入下面这个XML文件的数据：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP715.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们可以在Data面板上单击Create sample data按钮，然后选择Import Sample Data from XML&amp;#8230;：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP716.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的&lt;a href="http://msdn.microsoft.com/en-us/library/ee341393(v=Expression.40).aspx"&gt;Import Sample Data from XML对话框&lt;/a&gt;里，单击Browse按钮浏览并指定数据文件，然后按OK关闭对话框：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP717.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Expression Blend会很努力地在后台帮你生成一大堆东西，等它做完之后，你会看到Data面板上多了一堆东西，现在，确保Data面板上的List Mode按钮处于按下状态，然后把courseCollection拖到Pivot项标题下面的空白处：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP718.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Expression Blend会为你创建一个ListBox，并把它的ItemsSource属性绑定到courseCollection上，现在，右击ListBox里的任何地方，然后选择Auto Size\Fill，以便让ListBox充满整个Grid（Pivot项默认有一个Grid子元素）：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP719.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;嗯，不错，每个列表项都包含了课程名称、上课时间、下课时间以及上课地点，可是，这些内容各占一行，字体大小也是一样，每个列表项之间又没有明显的间距，显然不是那么好看，下面我们给它调整一下，右击ListBox里的任何地方，然后选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Edit Current进入列表项模板的编辑状态：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP720.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Objects and Timeline面板会发生变化，上面的对象不再是我们之前看到的那些，而变成列表项里的对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP721.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从上图不难看出，每个列表项都包含了四个TextBlock，这些TextBlock是用一个StackPanel装着的。现在，你可以发挥你的创造力，把它调整成你喜欢的样子，下面是我的调整结果：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP722.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到这里，你可能会问，怎么让上课时间和下课时间水平排列呢？很简单，你可以把它们放在一个StackPanel里，然后把StackPanel的Orientation属性的值设为Horizontal就行了。此时，Objects and Timeline面板上面的对象应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP723.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击上图红框那个箭头退出列表项模板的编辑状态。此时，Objects and Timeline面板回复"原状"了，你可以在上面看到Pivot控件和Pivot项。选中第二个Pivot项，按照上面的步骤把星期二的课程数据导入，并把它拖到Pivot项上（注意，是第二个Pivot项哦），然后调整ListBox的大小，使之充满整个Pivot项。那么，列表项的显示方式怎么办？要重复编辑一次吗？当然不用！我们只需应用刚才那个就可以了。右击ListBox里的任何地方，然后选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Apply Resource\courseCollectionItemTemplate：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP724.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，你会发现列表项的风格已经变成和前面的一样了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP725.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，我们现在可以把其它几天的课程都加上去，然后在主页（即第一个页面）添加一个按钮打开这个页面就&amp;#8230;&amp;#8230;慢着！我怎么编辑课程表？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;编辑课程表&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;显然，如果这个课程表不能编辑，那么它就等同花瓶了，所以我们要为它增加编辑功能，包括新建、编辑和删除，我们可以把这些功能放在Application Bar上。在Expression Blend里添加Application Bar非常简单，右击Objects and Timeline面板上的PhoneApplicationPage，然后选择Add ApplicationBar：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP726.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着右击ApplicationBar，然后选择Add ApplicationBarIconButton：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP727.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;确保刚才添加的Application Bar按钮处于选中状态，在Properties面板上把它的IconUri属性值改为New，并把它的Text属性值改为"新建"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP728.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;按照上面的步骤添加另外两个按钮，完成之后你的课程表页面应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP729.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;删除功能只需获取选中的课程并把它删除就可以了，新建和编辑则不同，它们都需要另一个页面来处理，我们知道，新建功能和编辑功能在用户界面上的最大区别是前者的页面有内容而后者的没有，所以我们可以为它们创建一个共用页面。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;创建一个Windows Phone Page，并把它命名为NewOrEditCoursePage.xaml：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP730.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;完了之后把ApplicationTitle的Text属性值改为"课程表"，但PageTitle保留原样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP731.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;因为新建功能和编辑功能共用同一个页面，所以PageTitle的Text属性值可能是新建课程或者编辑课程，这将会在打开此页面时通过传入参数设置。标题下面那块空地将会放置四个控件，分别对应课程名称、上课时间、下课时间和上课地点，首尾两个将会是TextBox控件，而中间两个将会是&lt;a href="http://silverlight.codeplex.com/releases/view/52297"&gt;Silverlight for Windows Phone Toolkit&lt;/a&gt;的TimePicker控件。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;右击Projects面板上的References节点，选择Add Reference&amp;#8230;：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP732.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;在弹出的Add Reference对话框里把C:\Program Files\Microsoft SDKs\Windows Phone\v7.0\Toolkit\Sep10\Bin\Microsoft.Phone.Controls.Toolkit.dll引用进来。添加完引用之后就可以把控件添加到页面了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP733.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，TimePicker控件已经自带标题功能，你只需设置它的Header属性就可以了，而普通的TextBox没有标题功能，只能自行添加TextBlock来模拟，为了使它的颜色和TimePicker控件的标题的颜色一样，我们需要把它的Foreground属性值改为PhoneSubtleBrush。此时，我的Objects and Timeline面板是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP734.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，课程表的用户界面已经设计完了，是不是很想看看运行效果呢？没问题！&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;回到CourseTimetablePage页，在Objects and Timeline面板上选中第一个Application Bar按钮，然后在Properties面板上单击Events，并双击Click旁边的编辑框：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP735.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Expression Blend会打开CourseTimetablePage页的代码隐藏文件，并为Application Bar按钮添加一个事件处理程序方法，我们只需在TODO下面加上一句就可以了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP736.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，回到MainPage页，把上面的TextBlock去掉，拖一个Button到中间，然后右击这个Button，选择Navigate To\CourseTimetablePage：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP737.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，按F5吧：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP738.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当你单击屏幕中间那个按钮时，课程表就会显示：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP739.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;向左或者向右滑动屏幕可以在不同的Pivot项之间来回切换，而向上或者向下滑动屏幕则可以查看当天课程。当你单击Application Bar上的新建按钮时，新建课程表的界面将会显示：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP740.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;你可以输入课程名称和上课地点，当你单击上课时间或者下课时间下面那个TimePicker控件时，设置时间的界面将会显示：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP741.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;你可以通过滑动设置时间，这个界面下面有两个Application Bar按钮，左边那个是确定，右边那个是取消，但为什么这两个图标是一样的？其实它是找不到图标才这样的，如果你下载了它的代码，你会在&lt;a href="http://silverlight.codeplex.com/SourceControl/changeset/view/56962"&gt;TimePickerPage.xaml&lt;/a&gt;里看到它已经把图标位置硬性规定为/Toolkit.Content/ApplicationBar.Check.png和/Toolkit.Content/ApplicationBar.Cancel.png了，你可以在PhoneToolkitSample\Toolkit.Content文件夹里找到这两个图标，在项目里创建一个Tooklit.Content文件夹，把它们复制进去，并把它们的Build Action设置为Content，重新运行就能看到了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;到目前为止，我们只写了一行代码（其实这行代码也可以省掉的），应用程序的功能和操作就基本上体现出来了，此时，你可能会问，在我们设计用户界面的过程里，Expression Blend到底在背后为我们做了什么呢？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;Expression Blend如何提供设计时数据的支持？&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先我们来看看Expression Blend为我们生成了哪些文件：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP742.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;由于我们的数据是从XML文件导入的，所以你会看到一个XSD文件，这是从我们那个XML文件生成的XML Schema，这个XSD文件将会用来生成相关的类，这些类都放在对应的C#文件里，而我们的数据最终是以XAML的形式存在的。当我们导入XML数据时，Expression Blend不但为我们生成这些文件，还在App.xaml的Application.Resource里添加了相应的对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP743.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;为什么添加到App.xaml而不是某个页面的XAML文件里呢？这是因为我们在Import Sample Data from XML对话框里选择了Define in Project（参见图16），如果我们当时选择Define in This document，它就会添加到某个页面的XAML文件里。那么，Expression Blend又是如何得知我们的数据保存在哪个XAML文件里呢？答案就在courses类的构造函数里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP744.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个URI看起来有点古怪，如果你用Visual Studio打开这个项目，你将会看到这个XAML文件的Build Action是Page，事实上，它会被编译成BAML，并且嵌到Iridescent程序集里，这种古怪的URI就是引用程序集内嵌资源的表示方式。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接着，当我们从Data面板把courseCollection拖到Pivot项上时（参见图17），Expression Blend会把它所属的MondayCoursesSampleData绑到Pivot控件的父容器的DataContext属性上，在当前Pivot项的子容器里创建一个ListBox，并把courseCollection绑到它的ItemsSource属性上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP745.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 5&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;还记得Import Sample Data from XML对话框里有个选项是Enable sample data when application is running吗？当时它是选中的，如果我们把这个选项去掉，DataContext属性前面就会多一个"d:"：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP746.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 6&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个前缀告诉编译器在生成最终程序集时忽略这个属性，这样你就不会在程序运行的时候看到这些数据了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;如果你细心观察Projects面板，可能会发现几个我们从未提及过的文件：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP747.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;它们是干嘛的呢？事实上，这些文件在我们刚创建完项目时就存在了，还记得最初的Panorama页吗（参见图3和图4），里面的Panorama项是有数据的，而这些数据就是来自MainViewModelSampleData.xaml文件的。那么，这些数据又是如何关联到Panorama控件的呢？打开MainPage.xaml，在文件的顶部，你会发现它的蛛丝马迹：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP748.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 7&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这里使用的不是我们常见的Binding，而是d:DesignData，正如你所看到的，它用来指定数据文件的位置，但这些数据是在设计时使用的，所以你会看到DataContext前面有个"d:"，事实上，我们把这些带有"d:"前缀的属性称为&lt;a href="http://msdn.microsoft.com/zh-cn/library/ff602277(VS.95).aspx"&gt;设计时属性&lt;/a&gt;。和这些数据相关的类分别定义在MainViewModel.cs和ItemViewModel.cs文件里。如果你打开MainViewModel.cs，你会发现里面的LoadData方法包含了另一份不同的数据，为什么，它们分别用来干嘛的？我给你截两个图，看你能否找到什么蛛丝马迹：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP749.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 8&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP750.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 9&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;看到了吗，XAML文件里每个ItemViewModel对象的LineOne属性值都是"design XXX"，而C#文件的则是"runtime XXX"，事实上，这已经道出它们的用途了，XAML文件里的数据是设计时使用的，而C#文件里的则是运行时使用的。但是，MainPage.xaml的根元素的DataContext属性前面有"d:"前缀啊，之前不是说编译器会在生成程序集的时候忽略它吗，那应用程序又如何找到运行时使用的数据呢？答案就在MainPage.cs里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP751.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 10&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;App类的ViewModel是一个静态属性，它的任务只是创建一个MainViewModel对象，当MainPage的构造函数被调用时，会把这个MainViewModel对象绑到自己的DataContext属性上，当Loaded事件触发时，如果数据还没装载，就调用LoadData方法装载数据。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;从上面的讨论不难看出，Expression Blend的确为我们做了很多，而这些知识将会协助我们完成后面的开发任务。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;保存课程表&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;说了那么多前端的东西，是时候看看后端了。课程表软件说到底就是一个管理课程表数据的软件，所以数据存储是一个非常重要的环节。如果说用户界面的设计是Expression Blend的强项，那么业务逻辑的开发就是Visual Studio的地盘，既然接下来的着眼点是后端，当然要切换到Visual Studio啦。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;右击Projects面板里的解决方案节点，选择Edit in Visual Studio：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP752.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 42&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Visual Studio会打开，接下来，我们将会在Visual Studio里完成后端部分的开发。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，创建一个Models文件夹，在里面添加一个Course类，并让它实现INotifyPropertyChanged接口：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP753.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 11&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，我们需要为它添加如下属性：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;     &lt;colgroup&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;col style="width:96px"&gt;&lt;/colgroup&gt;     &lt;tbody valign="top"&gt;         &lt;tr style="background: #9bbb59"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性名字&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性类型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;备注&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Name&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;string&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;课程名称&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Day&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;string&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;星期几&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;StartTime&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;DateTime&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;上课时间&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;EndTime&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;DateTime&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;下课时间&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #9bbb59 1.0pt; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;Location&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;string&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #9bbb59 1.0pt; border-right: solid #9bbb59 1.0pt"&gt;             &lt;p&gt;上课地点&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;     &lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 1&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这些属性实现起来并不难，但我们不能使用C# 3.0的自动属性来实现，因为我们需要在属性的set访问器里调用OnPropertyChanged方法，下面我们选择其中一个来实现：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP754.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 12&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;如果你觉得在创建这些属性时手动输入所有代码太麻烦，你也可以&lt;a href="http://msdn.microsoft.com/zh-cn/library/ms165396.aspx"&gt;创建一个代码段&lt;/a&gt;，然后通过代码段来添加属性。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接下来我们需要考虑一下以什么方式来存储课程表的数据，如果不考虑使用第三方类库的话，我们至少有JSON序列化、XML序列化以及LINQ to XML等几种常见的方式，而无论我们今天的选择是什么，将来都有可能换用别的方式，既然我们已经预见了变化，就应该通过抽象把它们隔离开来。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;创建一个Services文件夹，在里面创建一个ICourseStore接口，那么，我们又应该如何设计这个接口呢？毫无疑问，它应该以某种形式满足CRUD四种需求，那么，这种形式又是怎样的呢？回忆一下上一节的内容以及前面的用户界面，你觉得我们会对这个接口有什么期望？显然，如果我们能够获取一组Course对象，并把它们绑到Pivot项里的ListBox就好了。这个好办，我们可以在ICourseStore接口里声明一个Courses属性，类型是ObservableCollection&amp;lt;Course&amp;gt;，这样，数据绑定可以协助我们满足"R"的需求，当我们从用户界面上选中一个课程，单击Application Bar上的删除按钮，我们可以从ListBox上获取当前选中的Course对象，并从Courses属性里删除，这样，"D"的需求也得到满足了，剩下的就是"C"和"U"了，它们有一个共同的地方，就是都要打开一个新的页面，不同的是前者对应一个新的Course对象，而后者则对应当前选中的Course对象，当用户单击确定之后，这些改动就会提交到Courses属性，换句话说，四种需求都得到满足了。但是，这里好像没有提到把数据保存到独立存储区啊？没错，所以ICourseStore接口还需要提供一个Commit方法，把数据提交到独立存储区。此外，我们还可以提供一个Rollback方法，把Courses属性上的数据还原为独立存储区上的数据，这样，如果用户不小心把课程表改乱了，也无需通过重启应用程序来重新加载独立存储区上的数据。好了，ICourseStore接口确定下来了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP755.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 13&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;那么，接口的实现呢？刚才我们提到了三种常见的存储方式，我个人比较喜欢LINQ to XML，不过今天我想试一下JSON序列化。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，在Services文件夹里创建一个JsonCourseStore类，并让它实现ICourseStore接口：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP756.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 14&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，引用以下类库和命名空间：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;     &lt;colgroup&gt;&lt;col style="width:218px"&gt;&lt;/colgroup&gt;     &lt;tbody valign="top"&gt;         &lt;tr style="background: #f79646"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #f9b074 1.0pt; border-left: solid #f9b074 1.0pt; border-bottom: solid #f9b074 1.0pt; border-right: solid #f9b074 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;类库/命名空间&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr style="background: #fde4d0"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f9b074 1.0pt; border-bottom: solid #f9b074 1.0pt; border-right: solid #f9b074 1.0pt"&gt;             &lt;p&gt;System.Servicemodel.Web.dll&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f9b074 1.0pt; border-bottom: solid #f9b074 1.0pt; border-right: solid #f9b074 1.0pt"&gt;             &lt;p&gt;System.IO&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr style="background: #fde4d0"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f9b074 1.0pt; border-bottom: solid #f9b074 1.0pt; border-right: solid #f9b074 1.0pt"&gt;             &lt;p&gt;System.IO.IsolatedStorage&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f9b074 1.0pt; border-bottom: solid #f9b074 1.0pt; border-right: solid #f9b074 1.0pt"&gt;             &lt;p&gt;System.Runtime.Serialization.Json&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;     &lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 2&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，现在可以开始实现JsonCourseStore类了。当应用程序启动时，会调用JsonCourseStore类的构造函数，从独立存储区把课程表的数据反序列化到Courses属性里；而当用户执行回滚操作时，会调用Rollback方法，从独立存储区把课程表的数据反序列化出来，替换Courses属性的现有数据。感觉上，这两个方法所做的事情不尽相同，然而，当你开始实际的编码时，就会发现它们的代码其实是一样的，按照惯例，我们会把这部分代码提取到一个辅助方法里：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP757.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 15&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，构造函数和Rollback方法只需调用这个辅助方法就行了。值得提醒的是，在应用程序首次启动时，课程表的数据文件还不存在，此时，我们只需为Courses属性创建一个空的集合就行了。当用户执行提交操作时，会调用Commit方法，把Courses属性序列化到独立存储区：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP758.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 16&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，数据存储的开发也完成了，但是，我们怎么把它和前面设计的用户界面关联起来呢？这正是接下来要讲的。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;把前端和后端连接起来&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;还记得Expression Blend是怎么做的吗？它为MainPage页创建一个与之对应的MainViewModel类，并在代码隐藏文件里把后者的实例绑到前者的DataContext属性上，而剩下的事情就交给数据绑定来处理。接下来，我们将会模范这种做法，把前端和后端连接起来。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们知道，一个课程表包含若干列，每列都包含了一个标题和一组当天的课程，整个课程表对应于CourseTimetablePage页，里面的每列对应于一个Pivot项，其中，列的标题将会作为Pivot项的标题显示，而列所包含的那组当天的课程将会在Pivot项所包含的ListBox里显示。为了方便理解，我们把它们之间的映射关系制成下表：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;     &lt;colgroup&gt;&lt;col style="width:288px"&gt;&lt;col style="width:288px"&gt;&lt;/colgroup&gt;     &lt;tbody valign="top"&gt;         &lt;tr style="background: #f79646"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #f79646 1.0pt; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;页面&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;页面的抽象模型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;CourseTimetablePage页&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;CourseTimetableViewModel类&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt; Pivot项&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt; CourseTimetableColumnViewModel类&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt; Header属性&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt; Header属性&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt; ListBox控件&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt; Courses属性&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;     &lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 3&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们先来看看CourseTimetableColumnViewModel类。在ViewModels文件夹里创建一个CourseTimetableColumnViewModel类，并让它实现INotifyPropertyChanged接口，然后为它添加一个Header属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP759.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 17&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个属性将会在构造函数里初始化：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP760.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 18&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这些都不难，稍微有点麻烦的是Courses属性，我们该如何实现这个属性呢？目前，JsonCourseStore类的Courses属性返回的是全部课程，而CourseTimetableColumnViewModel类的Courses属性仅返回当天的课程，显然，我们要做过滤，此外，由于JsonCourseStore类返回的课程没有固定顺序，我们还要排序。一个可能的方案是通过LINQ从源集合筛选当天课程，并根据时间进行排序，然后添加到目标集合，我们还需要监听源集合的相关事件，以便在源集合的内容发生更改时把更改反映到目标集合。感觉上这个方案需要不少代码，还有没有别的方案呢？有，我们可以使用CollectionViewSource类，它能根据我们指定的过滤和排序条件提供集合视图，我们可以通过它的View属性访问这个视图：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP761.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 19&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;_courses的初始化也是在构造函数里进行：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP762.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 20&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，请思考一下，我们是不是直接给Source属性创建一个JsonCourseStore对象呢？回忆一下JsonCourseStore类的设计思路，CRUD四个操作都是直接在它的Courses属性上进行的，而C和U这两个操作是发生在另一个页面的（NewOrEditCoursePage页），这意味着我们需要一个全局的JsonCourseStore对象。解决方案有很多，你可以把JsonCourseStore类设计成单例模式，也可以使用依赖注入容器，而最简单的做法莫过于在App类里添加一个静态属性了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP763.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 21&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这样，Source属性的初始化就可以通过下面这句来完成了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP764.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 22&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;诚然，这并不是什么好的做法，你可能会坚持使用依赖注入容器，因为这样能更好的降低对象之间的耦合度，如果你已经知道怎么做，请不要犹豫，立即行动，鉴于这篇文章的内容已经很多了，所以我想把这个内容留到后面的文章。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;接着来看CourseTimetableViewModel类，在ViewModels文件夹里创建一个CourseTimetableViewModel类，并让它实现INotifyPropertyChanged接口。毫无疑问，它应该包含一组CourseTimetableColumnViewModel对象，我们可以创建一个Columns属性来存放它们：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP765.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 23&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此外，我们还需要一个属性来跟踪和当前显示的Pivot项绑定的是哪个CourseTimetableColumnViewModel对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP766.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 24&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个属性将会和Pivot控件的SelectedIndex属性进行双向绑定。这两个属性都会在构造函数里初始化：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP767.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 25&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;我们知道，用户体验很重要，我们应该尽可能减少用户获取所需信息的步骤，当用户打开课程表时最想看到的应该是今天的课程，这正是为什么我要把SelectedColumnIndex属性的值初始化为DateTime.Today.DayOfWeek属性的值。然而，当一种观点产生的时候，反面观点也会随之而来，有人可能建议每次打开课程表都能看到相同的东西，比如说，显示一周第一天的课程，也有人建议和上次离开该页面时的东西保持一致。事实上，这些观点没有谁对谁错，它们的目的都是为了改善用户体验，你可以按照你认为正确的去做，或许，我们也可以考虑在后续的版本里通过选项设置让用户选择。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，我们可以着手处理绑定了。打开CourseTimetablePage.xaml，切换到XAML模式，把现有的两个Pivot项删掉或者注释掉，因为我们不再单独处理个别Pivot项，而是把CourseTimetableViewModel类的Columns属性绑到Pivot控件的ItemsSource属性上，由Pivot控件动态创建Pivot项。此外，我们还要把CourseTimetableViewModel类的SelectedColumnIndex属性绑到Pivot控件的SelectedIndex属性上，并设置为双向绑定。设置好绑定后，Pivot控件的XAML应该是这样的：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP768.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 26&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;但是，Pivot控件又从何得知应该创建怎样的Pivot项呢？事实上，它无从知晓，也不知道该如何使用CourseTimetableColumnViewModel 类Columns属性，所以我们必须通过某种方式告诉它我们希望它怎么做，而这种方式就是数据模板。我们需要创建两个数据模板，一个用于Pivot项的标题，另一个用于Pivot项的内容，这两个数据模板将会放在页面的资源字典里。第一个数据模板很简单，只需创建一个TextBlock，并把Header属性绑到它的Text属性上：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP769.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 27&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;另一个数据模板也不难，你可以把其中一个Pivot项的XAML代码复制过来，并修改ItemsSource属性的绑定：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP770.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 28&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;之前Expression Blend为我们生成的Course类的属性采用全小写命名方式，而现在已经改为Pascal命名方式，所以courseCollectionItemTemplate数据模板的相关绑定也要改过来：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP771.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 29&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;数据模板创建好之后就可以应用到Pivot控件了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP772.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 30&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;由于我们不再使用Expression Blend生成的示例数据了，所以你可以把包含Pivot控件的Grid的DataContext属性去掉，然后在代码隐藏文件的构造函数里设置DataContext属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP773.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 31&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;好了，我知道你很心急了，按F5吧，然后单击中间的按钮&amp;#8230;&amp;#8230;噢！出错了！&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP774.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 43&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;怎么回事呢？经过一番调查，我发现问题出在代码25的设置SelectedColumnIndex属性那行，如果你去掉这行，你会发现什么问题也没有，如果你尝试为它硬编码一个值，你会发现同一个值有时候会抛异常，有时候不会，非常不稳定，为什么呢？原来，当我们设置SelectedColumnIndex属性的时候，它会把我们给它设的值同步到Pivot控件的SelectedIndex属性，但此时Pivot控件有可能还没完全构建好，于是就出错了。解决方法是把代码25出错的那行删掉，在代码31设置DataContext属性的那行后面加上这句：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP775.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 44&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;值得提醒的是，通过Loaded事件来设置SelectedColumnIndex属性是必须的，因为此时页面及其包含的控件已经构建好了，如果你只是把设置SelectedColumnIndex属性的代码从代码25复制粘贴到代码31，那么问题依旧存在。现在，当你打开课程表时，你会看到Pivot控件快速滑到今天的课程：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP776.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 45&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;哎哟，课程表空荡荡的，怎么检查课程的显示格式是否正确？添加几个看看吧。但是，添加课程的功能还没有&amp;#8230;&amp;#8230;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;操作课程表&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我们知道，新增和修改这两个操作是共用同一个页面的，但它们的内部逻辑又稍微有点不同， 你可以分别为它们创建两个不同的ViewModel类，针对不同的操作为页面创建不同的ViewModel对象，也可以在同一个ViewModel类里通过标记变量和条件语句区分两种不同的逻辑，你还可以像我这样，在ViewModels文件夹里创建一个NewOrEditCourseViewModel抽象类，让它实现INotifyPropertyChanged接口，并创建Title和Course两个属性以及Submit和Discard两个抽象方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP777.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 32&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在NewOrEditCourseViewModel类里创建NewCourseViewModel和EditCourseViewModel两个私有类，并让它们继承NewOrEditCourseViewModel类。下面，我们先看NewCourseViewModel类。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;课程表上的每个课程都会关联到一周的某天，但NewOrEditCoursePage页上却没有地方设置星期几（参见图32），这是为什么呢？Pivot控件的一个特点是每次只能显示一个Pivot项，这意味着整个课程表每次只能显示一天的课程，如果把这天看做上下文，当用户单击Application Bar上的新增按钮时，应用程序就可以从上下文获知应该把课程添加到哪一天，从而为用户省下设置星期几这个步骤。我们可以通过参数传递这个数据，然后在构造函数里使用它创建Course对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP778.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 33&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当用户单击确定时，就会把课程添加到JsonCourseStore，而单击取消的话就什么都不做：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP779.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 34&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;那么，EditCourseViewModel类呢？当用户单击Application Bar上的编辑按钮时，它需要的不是今天星期几，而是用户当前选中的课程是什么，我们又该如何告诉它呢？我们知道，同一天的课程在时间上是互斥的，因为同一时间你不可能在不同教室上课（除非你懂影分身术），换句话说，Course类的Day和StartTime这两个属性组合起来可以成为唯一标识。通过这个唯一标识，我们可以从JsonCourseStore里获取用户当前选中的课程，但是，我们是否把获取到的课程直接赋给EditCourseViewModel的Course属性呢？想想看，Course属性将会和NewOrEditCoursePage页上的控件进行双向绑定，当用户编辑控件的内容时，数据会直接反映在Course属性上，如果Course属性就是从JsonCourseStore里获取到的课程，那么数据就会直接提交到JsonCourseStore。如果你选择这样做，你得先做个备份，假如用户单击取消，你就可以把备份的数据还原回去。另一个做法是从JsonCourseStore里获取用户当前选中的课程，然后克隆一份赋给Course属性，当用户单击确定时，就把Course属性的数据更新过去，而单击取消的话也是什么都不做。这里选择后面那种做法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP780.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 35&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;创建好NewCourseViewModel和EditCourseViewModel两个私有类之后，我们需要考虑一下如何访问它们的实例，办法可能有很多，其中最简单的是在NewOrEditCourseViewModel类里创建两个静态方法，分别用于创建这两个类的实例：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP781.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 36&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;有了实例之后，我们就要考虑数据绑定的问题了，这个不难处理，我们总共也只有五个绑定需要创建，你可以参照下表修改NewOrEditCoursePage.xaml的内容：&lt;/p&gt;&lt;div&gt;&lt;table style="border-collapse:collapse" border="0"&gt;     &lt;colgroup&gt;&lt;col style="width:73px"&gt;&lt;col style="width:84px"&gt;&lt;col style="width:54px"&gt;&lt;col style="width:282px"&gt;&lt;/colgroup&gt;     &lt;tbody valign="top"&gt;         &lt;tr style="background: #f79646"&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #f79646 1.0pt; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;描述&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;类型&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;属性&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;&lt;span style="color:white"&gt;&lt;strong&gt;绑定表达式&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;页面标题&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;TextBlock&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;Text&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;{Binding Title}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;课程名称&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;TextBox&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;Text&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;{Binding Course.Name, Mode=TwoWay}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;上课时间&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;TimePicker&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;Value&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;{Binding Course.StartTime, Mode=TwoWay}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;下课时间&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;TimePicker&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;Value&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;{Binding Course.EndTime, Mode=TwoWay}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;         &lt;tr&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-left: solid #f79646 1.0pt; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;上课地点&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;TextBox&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt"&gt;             &lt;p&gt;Text&lt;/p&gt;             &lt;/td&gt;             &lt;td style="padding-left: 7px; padding-right: 7px; border-top: none; border-bottom: solid #f79646 1.0pt; border-right: solid #f79646 1.0pt"&gt;             &lt;p&gt;{Binding Course.Location, Mode=TwoWay}&lt;/p&gt;             &lt;/td&gt;         &lt;/tr&gt;     &lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;表 4&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;由于页面之间的切换是通过Navigate方法来实现的（参见代码2），而它又只接受Uri对象作为参数，于是查询字符串就自然而然地成为页面之间传递数据的主要途径了。我们需要的参数有三个：action、day和id，其中action的值有new和edit两种，分别对应新建和编辑两种操作，action可以和day或id搭配使用，但不会同时使用三个参数。我们可以根据这些参数的值创建对应的ViewModel对象，然后把它赋给页面的DataContext属性：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP782.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 37&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在的问题是，这段代码应该放在哪里？构造函数？还是别的什么地方？这取决于NavigationCacheMode属性的值是什么，如果它的值是Disabled，这意味着页面不会缓存，每次都会创建新的实例，那么我们可以把这段代码放在页面的构造函数里，否则，我们应该把它放在OnNavigatedTo方法里，事实上，任何时候我们来到一个页面，该页面的OnNavigatedTo方法都会被调用，所以放在这里比较保险。你可能会问，为什么CourseTimetablePage页的DataContext属性是在构造函数里设置的？好问题！那是因为CourseTimetablePage页的ViewModel对象由始至终都未曾改变，而NewOrEditCoursePage页的ViewModel对象及与之绑定的Course对象每次都可能不同，页面的缓存可能会导致用户看到"过期"的信息。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;当用户单击确定时，将会调用ViewModel对象的Submit方法，然后调用NavigationService对象的GoBack方法返回课程表；而单击取消的话就直接返回：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP783.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 38&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在，让我们回到CourseTimetablePage.cs，把新建按钮的Click事件处理程序改成下面这样：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP784.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 39&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;需要说明的是，Pivot项的标题是"星期X"，对应于Course对象的Day属性，因此我们可以把它作为参数通过查询字符串传给NewOrEditCoursePage页。接着，为编辑按钮创建一个Click事件处理程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP785.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 40&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;最后是删除按钮的Click事件处理程序：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP786.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 41&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;你可能会问，为什么执行删除之前不给用户提示一下？嗯，这是个值得考虑的问题，有时候你不提示用户会质问你万一删错了怎么办，有时候你提示了用户也会抱怨这样做很烦，一般而言，仅仅在执行操作之前给用户提示一下是远远不够的，你还需要让用户可以设置以后不再提示，以及告诉用户随时可以在哪里开启提示。不过，你可以尝试实现一个简单的提示，至于可以设置的提示将会在以后的文章里探讨。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在还缺什么吗？噢，对了，还缺两个菜单项，打开CourseTimetablePage.xaml，切换到XAML模式，在&amp;lt;shell:ApplicationBar&amp;gt;&amp;lt;/shell:ApplicationBar&amp;gt;里添加两个Application Bar菜单项：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP787.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 42&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;它们的Click事件处理程序比较简单，只是分别调用JsonCourseStore的Commit方法和Rollback方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP788.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 43&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我已经等不及要按F5了，单击主页中间的按钮打开课程表，单击新建按钮，输入课程名称：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP789.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 46&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;修改上课时间：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP790.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 47&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;单击确定返回：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP791.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 48&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;奇怪了！我刚才明明输入了课程名称，为什么现在没了呢？而且上课时间也没改过来！究竟发生了什么事？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;我查看了Silverlight for Windows Phone Toolkit的代码，在&lt;a href="http://silverlight.codeplex.com/SourceControl/changeset/view/56962"&gt;DateTimePickerPageBase类&lt;/a&gt;的HandleClosedStoryboardCompleted方法里可以看到，当我们设好时间并单击确定时，它会调用NavigationService对象的GoBack方法返回。前面我们提到，任何时候当我们来到一个页面时，该页面的OnNavigatedTo方法就会被调用，现在，我们在NewOrEditCoursePage.cs的OnNavigatedTo方法里设置一个断点，然后从设置时间的页面返回，看看会发生什么事：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP792.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 49&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;从OnNavigatedTo方法的e参数和NavigationContext对象的QueryString属性不难看出，GoBack方法并没改变导航URI，即查询字符串的action和day两个参数都完好如初，于是第一个条件语句被执行，创建一个全新的ViewModel对象并把它赋给页面的DataContext属性，就像刚刚打开这个页面似的，但是，此时页面的DataContext属性已经有ViewModel对象了呀！这正是问题所在，既然知道了原因，问题就不难解决了，我们只需在执行这块代码之前先判断一下DataContext属性是否为空就行了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在按F5重新运行应用程序，并新建一个课程，完了之后你就会在课程表里看到它了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP793.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 50&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;慢着！时间的显示格式有问题，我期望它显示为8:10 &amp;#8211; 10:00而不是现在这样，怎么办？这个时候就轮到转换器出场了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;首先，创建一个Utils文件夹，在里面添加一个TimeConverter类，并让它实现IValueConverter接口，实现这个接口只需实现两个方法，一个是Convert方法，用于把Course对象的StartTime属性和EndTime属性的值转换为显示在UI上的字符串，另一个是ConvertBack方法，这个方法只在双向绑定时才会派上用场，而这里是单向绑定，所以我们不必为它提供实现：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP794.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 44&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着在CourseTimetablePage.xaml的资源字典里添加一个TimeConverter对象：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP795.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 45&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;然后把那两个TextBlock的Text属性的绑定表达式分别改为"{Binding StartTime, Converter={StaticResource timeConverter}}"和"{Binding EndTime, Converter={StaticResource timeConverter}}"。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在按F5重新运行应用程序，并新建一个课程，这次时间的显示格式就没问题了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP796.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 51&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;选中这个课程，并单击编辑按钮：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP797.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 52&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;嗯，很好，页面标题和课程信息都正确显示了，修改一下并按确定返回：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP798.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 53&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;课程信息的更改也正确反映到课程表了。现在，确保课程处于选中状态，单击删除按钮，噢，出错了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP799.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 54&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个问题好解决，我们只需在使用e.Item之前判断一下它是否为null就行了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7100.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 46&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在按F5重新运行应用程序，并新建一个课程，先别删除这个课程，我们需要用它来执行以下测试：&lt;/p&gt;&lt;ol&gt;     &lt;li&gt;单击Application Bar右上角的省略号。此时，Application Bar的菜单项会显示出来。     &lt;/li&gt;     &lt;li&gt;单击"保存所有更改"菜单项，并按两次Back键退出应用程序。     &lt;/li&gt;     &lt;li&gt;从应用程序列表启动应用程序，并进入课程表。此时，刚才新建的课程应该显示在课程表里。     &lt;/li&gt;     &lt;li&gt;选中这个课程，单击编辑按钮，修改课程信息，并按确定返回。此时，课程信息的更改应该反映在课程表上。     &lt;/li&gt;     &lt;li&gt;单击Application Bar右上角的省略号，并单击"撤销所有更改"菜单项。此时，课程表上的课程信息应该还原为更改之前。但是，没有还原！！     &lt;/li&gt;     &lt;li&gt;确保这个课程处于选中状态，单击编辑按钮。此时，我们可以看到课程信息确实已经还原了！！     &lt;/li&gt;     &lt;li&gt;修改课程信息，并按确定返回。此时，我们可以看到新的更改没有反映在课程表上！！     &lt;/li&gt;     &lt;li&gt;确保这个课程处于选中状态，单击编辑按钮。此时，我们可以看到课程信息确实更改了！！     &lt;/li&gt;     &lt;li&gt;单击新建按钮，输入课程信息，并按确定返回。此时，我们在课程表上并没看到刚刚新建的课程！！     &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;为什么会这样？从上面的测试不难看出，所有怪事都是单击"撤销所有更改"菜单项之后发生的，而后面五条测试的结果显然在告诉我们课程表页面和JsonCourseStore已经脱节了。沿着这条线索，我们打开JsonCourseStore.cs文件，仔细阅读里面的代码，Rollback方法是直接调用LoadCoursesFromIsolatedStorage方法的，当我们单击"撤销所有更改"菜单项时，它会从独立存储区读取Courses.json文件，并把里面的数据反序列化到Courses属性。慢着！把数据反序列化到Courses属性？这会改变整个集合的引用！换句话说，每次调用Rollback方法时都会重新创建一个集合，而课程表却一直和"过期"的集合关联着，难怪后面几条测试给人的感觉是课程表页面和JsonCourseStore脱节了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;知道症结所在，剩下的事情就好办了，你可以分开实现JsonCourseStore的构造函数和Rollback方法，也可以像我这样，修改LoadCoursesFromIsolatedStorage方法：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7101.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 47&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;新版的LoadCoursesFromIsolatedStorage方法不再把数据直接反序列化到Courses属性，而是把Courses属性清空，再把数据逐条添加进去。至于Courses属性的初始化则挪到构造函数了。需要说明的是，ObservableCollection类没有ForEach方法，这是我自己创建的扩展方法，你也可以直接使用foreach语句实现。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;现在按F5重新运行应用程序，并重新执行一次上面的测试，嗯，这次没问题了，最后，选中这个课程，单击删除按钮，好了，课程已经删除了。在整个操作的过程中，还有两个地方我认为需要改善一下的。第一个是当用户单击"保存所有更改"菜单项时，应用程序应该在完成操作之后提示一下，否则用户可能会有点茫然。另一个是从NewOrEditCoursePage页返回CourseTimetablePage页时总是看到今天的课程，当我们新建或编辑的课程不是今天的课程时可能引起不必要的疑惑。试想一下，我在新建星期二的课程，当我输入完课程信息并按确定返回时，我看到星期一的课程，因为今天是星期一，这时我的感觉会是怪怪的，比较符合直觉的做法应该是返回时显示星期二的课程，而不是盲目地显示今天的课程。这两个改进的实现就当课后作业留给你吧，对于第二个问题，我可以给个提示，研究一下OnNavigatedTo方法和OnNavigatingFrom方法，应该难不了你吧？&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;菜单&amp;#183;样品菜色&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;还差什么呢？噢，对了，目前我们是通过主页中间的按钮打开课程表的，这显然不好意思拿出来见人，我们还是创建一个正式的主菜单吧。现在让我们切换到Expression Blend，如果你的Expression Blend已经关闭了，你可以右击Solution Explorer的MainPage.xaml，然后选择Open in Expression Blend：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7102.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 55&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;菜单的制作方式有很多种，这里选用ListBox：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7103.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 56&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;菜单创建好后，右击里面的"课程表"，然后选择Navigate To\CourseTimetablePage，此时，Expression Blend会为TextBlock添加一个NavigateToPageAction行为，但这个行为默认是关联到MouseLeftButtonDown事件的，这样，当用户按下"课程表"还没松开手就打开课程表了，而我们的习惯是松手之后才执行相关的操作，为了实现这个效果，我们可以把关联事件改为MouseLeftButtonUp：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7104.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 57&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;菜单有了，但样品菜色却被我们弄没了，如果你现在打开CourseTimetablePage.xaml，你将会看到此番情景：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7105.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 58&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;之前我们手动创建两个Pivot项，然后把它们分别绑到两个示例数据源，但现在Pivot项是通过数据绑定动态创建的，之前那些示例数据源就排不上用场了，既然用不了，那就删了吧，打开Data面板，右击数据源，然后选择Delete data source：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7106.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 59&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;此时，Expression Blend会把SampleData文件夹里的相关文件一并删除。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;在Expression Blend里，没有设计时数据会为调整控件模板和样式带来很大不便，下次你去找设计师调整一下用户界面，他们很可能会对你大骂一顿，那么，有没有办法让它再次显示设计时数据呢？答案是有的，而且不止一种，下面来看其中一种。我们知道，CourseTimetablePage页使用了数据绑定，而绑定表达式只提及了绑定的属性，并未提及属性所在的类型，换句话说，只要属性名字能够匹配，示例数据就应该绑得上去。首先，准备一份XML，注意匹配绑定表达式的属性名字：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7107.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 48&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;接着，在Data面板上把它导入（参见图15），注意，这次要把Enable sample data when application is running选项去掉：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7108.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 60&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;导入之后在Data面板上把自动生成的ColumnCollection和CourseCollection分别改为Columns和Courses，以便匹配绑定表达式的属性名字：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7109.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 61&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;现在，把Timetable拖到Pivot控件上，此时你会看到鼠标下方有个小提示，告诉你Expression Blend将会把Pivot控件的DataContext属性绑到CoursesSampleDataSource：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7110.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 62&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;当你松开鼠标时，就会看到课程名称了，但上课时间、下课时间和上课地点却显示不出来：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7111.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 63&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这是为什么呢？如果你把CourseTimetablePage页面关闭，然后重新打开它，你就会看到问题所在了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7112.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 64&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;这个异常明确地告诉我们调用转换器时抛出InvalidCastException。还记得吗，我们的转换器会把传入对象强制转换成DateTime对象，然后调用它的ToShortTimeString方法，但Expression Blend为示例数据生成的StartTime属性和EndTime属性是字符串类型的！明白为什么会这样，问题就不难解决了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7113.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;代码 49&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;重新编译项目，然后重新打开CourseTimetablePage页，你就会看到设计时数据了：&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7114.png" alt="" /&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#4f81bd; font-size:9pt"&gt;&lt;strong&gt;图 65&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;好了，总算对设计师有个交代了。&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style="color:#0070c0"&gt;&lt;strong&gt;下课了&amp;#8230;&amp;#8230;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/allenlooplee/111710_0006_WP7115.png" alt="" /&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/allenlooplee/aggbug/1879489.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/allenlooplee/archive/2010/11/17/1879489.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/allenlooplee/archive/2010/01/27/1657164.html</id><title type="text">Ruby 101：行为驱动</title><summary type="text">Ruby 101：行为驱动 Written by Allen Lee 写下你的期望 在上一篇文章里，我们创建了一个简易的插件系统，还为它写了一个YAML导出器，这次，我们将会尝试写个SQLite导出器，并探讨开发过程中的遇到的问题。 首先，我们的插件系统会自动装载插件，这意味着，当应用程序启动好后，插件就应该准备就绪了，这既是我们期望的效果，也是将来测试的时候需要覆盖到的内容，其重要性犹如航标...</summary><published>2010-01-27T00:25:00Z</published><updated>2010-01-27T00:25:00Z</updated><author><name>Allen Lee</name><uri>http://www.cnblogs.com/allenlooplee/</uri></author><link rel="alternate" href="http://www.cnblogs.com/allenlooplee/archive/2010/01/27/1657164.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/allenlooplee/archive/2010/01/27/1657164.html"/><content type="text">Ruby 101：行为驱动 Written by Allen Lee 写下你的期望 在上一篇文章里，我们创建了一个简易的插件系统，还为它写了一个YAML导出器，这次，我们将会尝试写个SQLite导出器，并探讨开发过程中的遇到的问题。 首先，我们的插件系统会自动装载插件，这意味着，当应用程序启动好后，插件就应该准备就绪了，这既是我们期望的效果，也是将来测试的时候需要覆盖到的内容，其重要性犹如航标...</content></entry></feed>
