从头开始构建 Windows Phone 7 应用






4.95/5 (36投票s)
构建一个 WP7 浏览器应用来访问 last.fm
引言
几年前发誓不再使用 Windows Phone 后,我又重新拥有了一部闪亮的新 HTC HD7。我过去曾在 CE 平台上尝试过开发移动应用,虽然很有趣,但它们始终无法与 iPhone 和 Android 平台上的应用相媲美。我甚至尝试过 在 CE 6 上模拟 iPhone 的外观和感觉。但问题始终在于 CE 不如其他操作系统好用,而且我从未能下定决心学习 iPhone 或 Android 的技术栈(因为时间和智力有限)。但现在,有了 Windows Phone 7 平台,根据我最初几周的使用体验,它看起来是一个真正的竞争者。而且最棒的是,我可以使用 C#、XAML 以及我已知的其他 MS 技术;无需再学习全新的技能(好吧,至少不是完全全新的——WP7 和桌面操作系统之间的平台差异仍然存在很多挑战)。
因此,这是我第一次尝试开发 Windows Phone 7 应用。它是一个用于在线音乐服务 last.fm 的浏览器。Last.fm 是一个社交媒体/音乐服务,可以提供音乐推荐并让你与品味相似的人联系。这个应用不包含音乐内容的流媒体播放(因为说实话,我弄不清楚是否可以合法地这样做),但它提供了对 last.fm 账户相关的大部分其他内容的访问。
如果你喜欢 last.fm 并想使用它,它已经在 应用中心上线了。
背景
必备组件
要玩转这些代码,你需要具备以下条件:
- WP7 SDK 和相关工具 - 这是显而易见的。
- GalaSoft 的 MVVM Light - 用于 MVVM 支持(命令、ViewModel、Mediator 等)。
- Silverlight for Windows Phone Toolkit - 用于一些额外的控件,例如 `WrapPanel`。
- last.fm API 密钥 - last.fm API 访问必须包含一个与特定用户关联的密钥签名。这些密钥很容易获取且免费。我已经从源代码中移除了我的密钥,并用 `#warning` 语句替换它们,以便你知道在哪里插入你自己的密钥。
- BING Maps 密钥 - 我在几个地方使用了 Bing Maps 控件。这同样需要一个密钥,我已经移除了我的密钥,并指出了在哪里插入你的密钥。
- last.fm 账户 - 如果你没有 last.fm 服务账户,将看不到太多内容。
我还从网上收集了一些其他代码片段。我将在后续内容中指出它们。
Using the Code
数据层和模型
让我们从数据层和模型开始。Last.fm 以一组 RESTful 服务暴露其 API。首先需要做的是与这些服务进行通信。它们提供 XML 或 JSON 响应的选项。我选择了 XML,主要是因为我对 XML 比较熟悉,而尚未深入研究 JSON。
接收数据
我最初尝试将 LastFmSharp 移植到 Silverlight,但结果变得很混乱,从未真正奏效。然后我开始寻找是否能让 RIA Rest Services 完成这项工作。在尝试了一番但没有成功后,我尝试了 `ServiceModel` 命名空间。我相信有内置的 WCF 方法来访问 last.fm 服务,但我未能让任何方法奏效,所以我决定手动进行连接。我告诉你,没有“右键单击 | 添加服务引用”生成的类型,手动连接 REST 服务并非易事。last.fm 服务不提供 WSDL 类元数据,并且它们提供的 XML 结构并非最干净(即,它们的结构似乎在不同方法调用之间有所变化。有时,`
总而言之,从头开始连接一个小型 REST 客户端还挺有趣的,而且并不太难。此外,能够完全控制对象的反序列化,使我能够处理 API 的特殊性。
所以,让我们再次从最底层开始:读写数据。读写是通过 `RemoteMethod` 对象完成的。基类封装了远程方法的名称和参数,以及 last.fm 所需的特定消息签名。
注意:last.fm 需要对消息进行 MD5 哈希处理。WP7 不包含 Md5CryptoServiceProvider,因此包含了一个来自 MSDN 的 MD5 实现。 格式化请求参数的细节可以在 `DictionaryExtensions` 类中找到。
读取数据
所有 HTTP 通信都通过 `WebClient` 处理,这使得通信非常容易。例如,所有 XML 的检索都只需以下代码:
protected override void InvokeMethod(object state)
{
WebClient client = new WebClient();
client.DownloadStringCompleted += new DownloadStringCompletedEventHandler
(client_DownloadStringCompleted);
UriBuilder builder = new UriBuilder(RootUri);
builder.Query = Parameters.ToArgList();
client.DownloadStringAsync(builder.Uri, state);
}
void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
RemoteMethodCallBacks callbacks = (RemoteMethodCallBacks)e.UserState;
try
{
XDocument doc = XDocument.Parse(e.Result);
callbacks.ReturnSuccess(doc);
}
catch (WebException ex)
{
callbacks.ReturnError(CreateErrorDocument(ex));
}
finally
{
((WebClient)sender).DownloadStringCompleted -=
new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
}
}
由于所有响应都是 XML,因此解析在数据接收处进行。令我有些意外的是,`XmlDocument` DOM 模型并未包含在 WP7 中,但我正在适应 `XDocument` 等;尽管我很怀念 XPath。如果你想切换到 JSON,代码将在此处更改。`RemoteMethodCallbacks` 对象持有两个 `Action
public void Invoke(Action<XDocument> successCallback, Action<XDocument> errorCallback) { InvokeMethod(new RemoteMethodCallBacks(successCallback, errorCallback)); }
填充对象模型
因此,在成功从 Web 服务获取一些数据后,我认为自己已经大功告成了。稍微修改一下 XAML UI,搞定。但事与愿违。
我一直有一个观点,那就是一旦有了 XML,你就拥有了一个模型,而且我从未觉得有必要将 XML 转换为 C# 对象。因此,我一直避免对象序列化和反序列化。这似乎是多余的一步;尤其是在你拥有 WPF 绑定等技术触手可及,可以直接操作 XML 时。据我所知,LINQ to XML 数据绑定在 WP7 上不受支持。真是没办法。看来我们还是需要对象反序列化。
身份验证
但在我们开始做这些之前,我们需要向 last.fm 进行身份验证并获取一个会话密钥。Last.fm 会话密钥用于标识用户且不会过期,因此一次登录可以持续多个会话。为此,一旦我们成功获取会话密钥,它就会被保存到 `IsolatedStorage` 中,这样用户就不需要再次登录。`IsolatedStorage` 是 WP7 应用唯一可以访问的本地 IO。
[DataContract]
public class Session
{
[DataMember]
public string SessionKey { get; set; }
public void Authenticate(string username, string md5Password,
Action successCallback, Action<XDocument> errorCallback)
{
Dictionary<string, string> p = new Dictionary<string, string>();
p["username"] = username;
p["authToken"] = MD5Core.GetHashString(username + md5Password);
RemoteMethod method = new RemoteWriteMethod("auth.getMobileSession", p);
Debug.Assert(successCallback != null);
method.Invoke(doc =>
{
User.Name = username;
SessionKey = doc.Descendants("key").First().Value;
successCallback();
},
errorCallback
);
}
}
public partial class App : Application
{
public static void SaveStateToIsolatedStorage()
{
using (var applicationStorage = IsolatedStorageFile.GetUserStoreForApplication())
using (var settings = applicationStorage.OpenFile
("settings.xml", FileMode.Create, FileAccess.Write, FileShare.None))
{
var document = new XDocument(new XDeclaration("1.0", "utf-8", "yes"),
new XElement("settings",
new XElement("timeStamp", DateTime.Now),
new XElement("sk", Session.Current.SessionKey),
new XElement("user", Session.Current.User.Name)
));
document.Save(settings);
}
}
}
填充对象
一旦我们拥有了用户名和会话密钥,就可以调用 last.fm 的其他任何方法。在这个应用中,根对象是 `User`,它对应于已认证的账户。从 `User` 对象,我们可以加载他们朋友列表、音乐库、最近播放的曲目等。所有这些都是 `ObservableCollections`,并且从这些集合中,我们可以导航到单个 `Artist`、`Album` 和其他感兴趣的类型。
[DataContract(Name="user")]
[InitMethod("user.getInfo")]
public class User : INotifyPropertyChanged, IDisplayable, IShoutable
{
[DataMember(Name="name")]
public string Name { get; set; }
[CollectionBindingAttribute("artist", "user.getRecommendedArtists")]
[DataMember]
public ObservableCollection<Artist> RecommendedArtists { get; set; }
}
`DataContractAttribute` 和 `System.Runtime.Serialization` 命名空间中的其他属性用于两个目的:
- 在页面生命周期内将模型对象序列化和反序列化为页面状态(稍后详述)
- 使用元数据标记类型和成员,这些元数据描述了 last.fm XML 结构,这些结构将在填充来自 last.fm 数据的模型对象时被反序列化
由于在使用 `System.Xml.Serialization` 进行对象反序列化时,在映射 last.fm XML 结构方面遇到了一些初始困难,我采用了自己编写的轻量级实现,而不是使用内置的 `XmlSerializer` 对象。这让我能够更快地推进,而且似乎用很少的代码就能很好地工作。它使用了 `DataContract` 属性以及一些额外的属性来帮助处理 last.fm 的特定习语。
其中最有趣的是 `CollectionBindingAttribute`,它描述了可用于填充特定 `ObservableCollection` 的远程方法和 XML 元素名称。
[CollectionBinding("artist", "user.getRecommendedArtists")]
[DataMember]
public ObservableCollection<Artist> RecommendedArtists { get; set; }
在上面的示例中,`CollectionBinding` 指示该集合是通过调用 `user.getRecommendedArtists` 方法来填充的,该方法将返回一个 `
public class RemoteCollectionLoader<T> where T : new()
{
public void Load(ICollection<T> collection, Action success, Action<XDocument> fail)
{
if (Parameters.ContainsKey("sk") && !string.IsNullOrEmpty(Parameters["sk"]))
{
RemoteMethod method = new RemoteReadMethod(MethodName, Parameters);
method.Invoke(
d => { OnCollectionLoaded(collection, d); if (success != null) success(); },
d => { if (fail != null) fail(d); });
}
}
private void OnCollectionLoaded(ICollection<T> collection, XDocument data)
{
// loop over the collection and create/bind new objects; adding them to the list
foreach (XElement e in data.Descendants(ElementName))
{
T content = new T();
RemoteObjectFactory.Load(content, e);
if (!collection.Contains(content))
collection.Add(content);
}
}
}
这就是填充整个对象模型的关键方法。一个对象拥有属性(所有这些属性都是 `string`)以及相关对象的 `ObservableCollection`。一个 `Artist` 拥有 `Name` 和其他元数据,以及 `Shout`、`Album` 和类似艺术家。它就是应用填充用户库、日历以及朋友和邻居列表的方式。
写入数据
修改服务器上的数据需要将请求 POST 到 last.fm 服务。`RemoteWriteMethod` 通过 `WebClient` 以 `x-www-form-urlencoded` 的形式发送方法参数来执行此操作。在此示例中,`Shout` 方法将在用户的个人资料上发布一条消息。
public class User : INotifyPropertyChanged, IDisplayable, IShoutable
{
public void Shout(string msg)
{
var args = new Dictionary<string, string>();
args["user"] = Name;
args["message"] = msg;
args["sk"] = Session.Current.SessionKey;
var method = new RemoteWriteMethod("user.shout", args);
method.Invoke(null, null);
}
}
internal class RemoteWriteMethod : RemoteMethod
{
protected override void InvokeMethod(object state)
{
WebClient client = new WebClient();
client.Headers["Content-type"] = "application/x-www-form-urlencoded";
client.UploadStringCompleted += new UploadStringCompletedEventHandler(client_UploadStringCompleted);
client.UploadStringAsync(RootUri, "POST", Parameters.ToArgList(), state);
}
}
ViewModels
`ViewModels` 处理 UI 与模型之间的交互,因此此项目中的 `ViewModels` 与其他项目没有区别。网上有很多关于 MVVM 的很好的信息,所以我不打算详细介绍 `ViewModel` 是什么以及它是如何工作的。相反,我将指出几个对 WP7 和这个应用特别有趣的点。
处理导航命令
`AppViewModel` 基类处理来自 UI 的导航请求,并判断是否打开外部浏览器。这取决于请求 URI 是相对的还是绝对的。绝对地址是应用本身之外的,因此必须在 `WebBrowserTask` 中打开。`WebBrowserTask` 是测试应用如何处理“断言”(tombstoning)的一个好方法,因为它会在用户使用 `WebBrowser` 时将你的应用置于休眠状态,并在用户导航返回时将其唤醒。我们将在“视图”部分更详细地介绍“断言”。
public abstract class AppViewModel : ViewModelBase
{ protected void Navigate(string address)
{
if (string.IsNullOrEmpty(address))
return;
Uri uri = new Uri(address, UriKind.RelativeOrAbsolute);
if (uri.IsAbsoluteUri)
{
WebBrowserTask browser = new WebBrowserTask();
browser.URL = address;
browser.Show();
}
else
{
Debug.Assert(App.Current.RootVisual is PhoneApplicationFrame);
((PhoneApplicationFrame)App.Current.RootVisual).Navigate(uri);
}
}
protected void Navigate(string page, AppViewModel vm)
{
string key = vm.GetHashCode().ToString();
ViewModelLocator.ViewModels[key] = vm;
Navigate(string.Format("{0}?vm={1}", page, key));
}
}
视图/页面之间导航
主要的应用程序 `ViewModels`(profile、library、people 和 calendar)通过 MVVM 的 `ViewModelLocator` 绑定到它们的视图。这是因为这些 `ViewModels` 是 `static` 的,并且可供整个应用访问。当 UI 中选择了一个项目时,我们需要动态创建一个 `ViewModel` 并将其绑定到 `View`。这是通过将其添加到 `static` 的 `ViewModels` 集合中,并在 URI 参数列表中通过键来传递给 `View` 来实现的。
public abstract class RemoteObjectViewModel<T> : DisplayableViewModel<T> where T : IDisplayable
{
protected RemoteObjectViewModel(T item)
: base(item)
{
SelectItemCommand = new RelayCommand(SelectItem);
}
protected virtual void SelectItem()
{
}
public RelayCommand SelectItemCommand { get; private set; }
}
public class AlbumViewModel : RemoteObjectViewModel<Album>
{
protected override void SelectItem()
{
Navigate("/AlbumPage.xaml", this);
}
}
在视图(一个 `PhoneApplicationPage`)中,我们然后在这个集合中查找 `ViewModel`。
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (NavigationContext.QueryString.ContainsKey("vm"))
{
string vm = NavigationContext.QueryString["vm"];
if (ViewModelLocator.ViewModels.ContainsKey(vm))
Dispatcher.BeginInvoke(() => { DataContext = ViewModelLocator.ViewModels[vm]; });
}
base.OnNavigatedTo(e);
}
当页面向后导航离开时,它将不再可达,因此我们可以从集合中移除 `ViewModel` 并清理它。
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
AppViewModel vm = DataContext as AppViewModel;
if (vm != null)
{
string key = vm.GetHashCode().ToString();
if (e.NavigationMode == NavigationMode.Back && ViewModelLocator.ViewModels.ContainsKey(key))
{
ViewModelLocator.ViewModels[key].Cleanup();
ViewModelLocator.ViewModels.Remove(key);
}
}
}
处理异步
当然,Web 客户端应用中最具挑战性的部分之一是处理异步数据传输。这个应用中的大多数异步调用都用于查询 last.fm 的对象集合并填充 `ObservableCollections`。大部分繁重的工作由上面显示的 `RemoteCollectionLoader` 处理。其中一些工作,特别是发起异步通信,是 `ViewModels` 的责任。
在这个例子中,`ProfileViewModel`(位于主 Profile 视图的后面)公开了一个 `RecommendArtists` 属性。当视图在绑定过程中调用此属性时,将从一个集合中检索一个 `ItemsSourceViewModel`。检索后,它将被告知异步加载其内容,然后返回给调用者。一旦数据检索完毕,集合也填充了模型对象,就会为集合中的每个项目创建一个新的 `ViewModel` 对象。视图绑定到 `ViewModelItems` 属性,该属性包含由此产生的 `ViewModel` 集合。
public class ProfileViewModel : DisplayableViewModel<User>
{
public ProfileViewModel(User u)
: base(u)
{
ViewModels.Args["user"] = Item.Name;
ViewModels.Add<Artist>(new Item>sSourceViewModel<User, Artist>
("RecommendedArtists", item => new ArtistViewModel(item)));
}
public AppViewModel RecommendedArtists
{
get
{
return ViewModels.GetViewModel<Artist>("RecommendedArtists", Item.RecommendedArtists);
}
}
}
public class ViewModelCollection<TParent> : INotifyPropertyChanged
{
private Dictionary<string, AppViewModel> _viewModels =
new Dictionary<string, AppViewModel>(StringComparer.Ordinal);
public ItemsSourceViewModel<TParent, TItem> GetViewModel<TItem>
(string name, ICollection<TItem> collection) where TItem : new()
{
if (_viewModels.ContainsKey(name))
{
var vm = (ItemsSourceViewModel<TParent, TItem>)_viewModels[name];
vm.Load(Args, collection);
return vm;
}
return null;
}
}
public class ItemsSourceViewModel<TParent, TItem> : AppViewModel where TItem : new()
{
public void Load(IDictionary<string, string> args, ICollection<TItem> items)
{
_items = items;
_args = args;
if (items.Count < 1)
{
Working = true;
var loader = RemoteObjectFactory.CreateLoader<TItem>(typeof(TParent), Name, args);
loader.Parameters["sk"] = Session.Current.SessionKey;
loader.Load(items, LoadComplete, SetLastError);
}
else
{
LoadComplete();
}
}
protected void LoadComplete()
{
ViewModelItems = _items.Select(item => factory(item));
Working = false;
}
private IEnumerable _viewModelItems;
public IEnumerable ViewModelItems
{
get { return _viewModelItems; }
protected set
{
_viewModelItems = value;
RaisePropertyChanged("ViewModelItems");
}
}
}
视图
代码后置基类
项目中的所有页面都继承自 `LastFmPage`,它提供了一些基本的共享功能,例如处理身份验证状态的变化。
页面生命周期
当一个页面被导航离开时,WP7 会将其置于休眠状态。当用户返回到该页面时,它会被反序列化并恢复。一个重新唤醒的页面应该重建自身,以便看起来与用户离开时相同。管理和恢复页面状态由基类提供帮助。由于在许多情况下,页面绑定到一个由用户交互决定且未静态附加到 `ViewModelLocator` 的 `ViewModel`,因此页面可能需要存储一些关于正在显示的内容的状态。WP7 提供了一个状态字典来实现此目的。为了能够存储在 `State` 字典中,对象必须是可序列化的。我选择存储 `Model` 对象(因为它们已经是完全可序列化的)以及 `ViewModel` 的类型,而不是序列化 `ViewModel` 实例,以便稍后可以实例化它。
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
AppViewModel vm = DataContext as AppViewModel;
if (vm != null)
{
object model = vm.GetModel();
if (model != null)
{
State["dctype"] = vm.GetType().AssemblyQualifiedName;
State["model"] = model;
State["stamp"] = DateTime.Now;
}
}
}
然后在页面重新填充时,它可以从状态字典中检索 `Model`,创建一个正确的 `ViewModel` 的新实例,并重新连接。
protected bool IsResurrectedPage
{
get
{
return _newPageInstance && this.State.ContainsKey("PreservingPageState");
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (IsResurrectedPage && State.ContainsKey("model") && State.ContainsKey("dctype"))
{
object model = State["model"];
Type t = Type.GetType(State["dctype"].ToString(), false);
if (t != null && model != null)
{
AppViewModel vm = Activator.CreateInstance(t, model) as AppViewModel;
if (vm != null)
{
if (State.ContainsKey("stamp"))
{
// if the data is more than an hour old refresh it
DateTime stamp = (DateTime)State["stamp"];
if (DateTime.Now - stamp gt; new TimeSpan(1, 0, 0))
vm.Refresh();
}
Dispatcher.BeginInvoke(() => { DataContext = vm; });
}
}
}
base.OnNavigatedTo(e);
}
ApplicationBar 事件处理
`ApplicationBar` 有点奇怪,无法数据绑定到 ViewModel。因此,页面基类负责处理应用栏事件。它使用 `IApplicationBarMenuItem` 的文本属性作为命令标识符并做出相应的响应。这不是最强大的系统,但它有效,并允许页面通过单个事件处理程序共享应用栏功能。希望有人能创建一个允许应用程序栏命令绑定的机制。
// in a LastFmPage derived class
private void ApplicationBar_Click(object sender, EventArgs e)
{
AppBarButtonPressed(((IApplicationBarMenuItem)sender).Text);
}
// in the LastFmPage class
protected virtual void AppBarButtonPressed(string text)
{
if (text == "logout")
{
AppViewModel vm = DataContext as AppViewModel;
if (vm != null && vm.SignOutCommand != null && vm.SignOutCommand.CanExecute(null))
vm.SignOutCommand.Execute(null);
}
else if (text == "refresh")
{
AppViewModel vm = DataContext as AppViewModel;
if (vm != null)
vm.Refresh();
}
else if (text == "profile")
{
NavigationService.Navigate(new Uri("/HomePage.xaml", UriKind.Relative));
}
else if (text == "library")
{
NavigationService.Navigate(new Uri("/LibraryPage.xaml", UriKind.Relative));
}
else if (text == "events")
{
NavigationService.Navigate(new Uri("/CalendarPage.xaml", UriKind.Relative));
}
else if (text == "people")
{
NavigationService.Navigate(new Uri("/NeighboursPage.xaml", UriKind.Relative));
}
else if (text == "search")
{
NavigationService.Navigate(new Uri("/SearchPage.xaml", UriKind.Relative));
}
else
{
AppViewModel vm = DataContext as AppViewModel;
if (vm != null && vm.Commands.ContainsKey(text))
{
var command = vm.Commands[text];
command.Execute(null);
}
}
}
TitlePanel
每个页面都包含一个 `UserControl`,它提供了一致的视觉标题,以及显示错误消息和进度指示器的机制。
<local:TitlePanelControl Grid.Row="0" VerticalAlignment="Top"/>
(`AppViewModel` 基类中都存在 Error 和 Working 属性)。
<TextBlock Foreground="Red" TextWrapping="Wrap"
Text="{Binding Error}"/>
<ProgressBar
IsIndeterminate="{Binding Working}"
Visibility="{Binding Working, Converter={StaticResource VisibilityConverter}}"
Style="{StaticResource CustomIndeterminateProgressBar}"/>
Tombstoning
与每个页面一样,整个应用程序也可能被置于休眠状态。当用户导航到另一个应用程序,例如显示 `WebBrowserTask` 或按下主页按钮时,就会发生这种情况。这被称为“断言”(Tombstoning),应用需要存储和检索状态,以便当它被导航回时,用户会觉得它从未消失过。
这主要包括响应应用程序上的四个 `static` 事件:
- Launching - 应用冷启动时。文档说不要在此事件中访问 `IsolatedStorage`,因为它会减慢启动速度。我在事件处理程序中启动一个后台线程来执行此操作,以便应用能够快速启动,然后在显示后进行填充。
- Activated - 在应用被“断言”后导航回应用时。`PhoneApplcationService` 提供了一个状态字典供应用检索状态,可在此处使用。
- Deactivated - 应用正在被“断言”。将状态保存到 `PhoneApplcationService` 和 `IsolatedStorage`(因为此逻辑应用实例可能永远不会被取消“断言”)。
- Closing - 当应用从导航堆栈中移除,无法再导航回时。在此处将状态保存到 `IsolatedStorage`。
// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
RootFrame.Dispatcher.BeginInvoke(LoadStateFromIsolatedStorage);
}
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
LoadStateFromService();
}
// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
SaveStateToService();
SaveStateToIsolatedStorage();
}
// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
SaveStateToIsolatedStorage();
}
用户界面
UI 的根是四个页面,分别显示用户的个人资料、朋友、库和事件。每个页面在其他主页的 `ApplicationBar` 上都有一个条目。每个页面都包含一个 `Pivot` 控件,将页面划分为可导航的子项列表。每个子项(如艺术家、事件或人物)本身都有一个页面,其中包含一个 `Pivot` 控件,将该项分解为更多细节,并允许用户深入浏览其 last.fm 账户内容。
因此,大部分 UI 由 `Pivot` 控件组成。当它们显示图像网格时,这些图像是在 `WrapPanel` 中显示的clickable buttons(可点击按钮)。
<ItemsPanelTemplate x:Key="PivotItemPanelTemplate">
<toolkit:WrapPanel ItemHeight="228" ItemWidth="228"/>
</ItemsPanelTemplate>
<DataTemplate x:Key="PivotItemDataTemplate">
<Button Style="{StaticResource ImageButtonStyle}" CacheMode="BitmapCache"
cmd:ButtonBaseExtensions.Command="{Binding SelectItemCommand}">
<StackPanel Background="Transparent" >
<Image Stretch="UniformToFill" Width="220" Height="220"
Source="{Binding LargeImage}"/>
<Grid Background="Black" Margin="0,-20,0,0" Opacity="0.5"
Height="20"/>
<TextBlock Margin="0,-25,0,0" Width="218" Foreground="White"
Text="{Binding Name}" FontSize="{StaticResource PhoneFontSizeNormal}">
<TextBlock.Clip>
<RectangleGeometry Rect="0,0,218,150"/>
</TextBlock.Clip>
</TextBlock>
</StackPanel>
</Button>
</DataTemplate>
`ItemsSource` 用于 UI 中使用的 `WrapPanel`、`ListBox` 和 `ItemsControl`,它们获得一个 `DataContext`,该 `DataContext` 是一个 `ItemsSourceViewModel`(如上所述)。进而,它们的 `ItemsSource` 属性被绑定到 `ItemsSourceViewModel` 上的 `ViewModelItems` 集合。通过这种方式,当用户在应用中导航时,他们实际上是在导航 ViewModel,事物就这样自动连接起来。
<controls:PivotItem Header="artists" DataContext="{Binding RecommendedArtists}">
<ScrollViewer x:Name="RecommendedArtistsScrollViewer">
<ItemsControl
ItemsSource="{Binding Path=ViewModelItems}"
ItemsPanel="{StaticResource PivotItemPanelTemplate}"
ItemTemplate="{StaticResource PivotItemDataTemplate}"/>
</ScrollViewer>
</controls:PivotItem>
杂项
命令绑定
MVVM Light 的命令绑定对于将 UI 连接到 ViewModel 的 `ICommand` 非常有价值。
<HyperlinkButton Content="website"
cmd:ButtonBaseExtensions.Command="{Binding Path=NavigateCommand}"
cmd:ButtonBaseExtensions.CommandParameter="{Binding Item.Website}"
Visibility="{Binding HasWebsite, Converter={StaticResource VisibilityConverter}}"/>.
不确定进度条
每个页面顶部都有一个 `
当前版本的控件存在一个问题,它在 UI 线程上进行动画。因此,如果你的 UI 除了显示进度外还在执行其他操作,它可能会有些卡顿。MSDN 有一个代码片段将动画移到了合成器线程,我建议使用这种方法,因为它带来了显著的改进。
页面过渡
对动画页面过渡的原生支持不多,但有几种包含它的替代方案。现在 Silverlight Toolkit 有一个页面过渡解决方案。我采用了 Clarity consulting 的一些代码,效果相当不错。他们似乎对 WP7 非常了解,并且包含它非常简单,只需让我的页面基类继承他们的基类,然后在我的页面构造函数中设置 `AnimationContext` 属性即可。`AnimationContext` 决定了从页面到页面移动时哪个视觉元素将进行动画。如果未设置,它将对整个页面进行动画。我决定将其设置为页面内容,以便标题区域(last.fm logo)看起来在页面之间是静态的。
public partial class EventPage : LastFmPage
{
public EventPage()
{
InitializeComponent();
AnimationContext = ContentPanel;
}
}
Button TiltEffect
WP7 应用中的按钮具有倾斜行为,当它们被点击时会下压。这并非内置功能,但MSDN 上有代码可以创建相同的行为。它非常易于使用,你只需要在应用启动时静态启用它,它就会应用于你的 UI 创建的任何按钮。
public partial class App : Application
{
private void Application_Launching(object sender, LaunchingEventArgs e)
{
TiltEffect.SetIsTiltEnabled(RootFrame, true);
}
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
TiltEffect.SetIsTiltEnabled(RootFrame, true);
}
}
提交应用
将应用提交到应用中心非常容易。微软的网站会引导你完成整个过程,老实说,最困难的部分只是捕捉和调整图像及屏幕截图的繁琐工作。
第一次提交被拒绝了,原因是它在 WP7 的浅色主题下看起来不好(即,它不好看)。在提交之前我甚至没有考虑过浅色主题,所以请从我的错误中吸取教训。
- 除非你真的想要在两种主题上都使用这些颜色,否则不要设置 ApplicationBar 的颜色。
你无法对它们进行 `DataBind`,WP7 会自动为每种主题切换它们,但如果你在 XAML 中手动设置了颜色,则不行。 - 不要在 ApplicationBar 上使用彩色图标。
同样,它们在深色主题下可能看起来很棒,但切换到浅色主题时,它们最多会被绘制成黑色轮廓,最坏的情况下是黑色斑块。使用白色背景的透明图标。它们在两种主题下看起来都不错。网上有一些不错的资源。不要包含外圈(应用栏会添加它),并将图像本身设置为 24x24。
关注点
在编写这个应用的过程中,我不得不处理许多对我来说是全新的事情:REST、Silverlight、WP7 平台。下次我会做一些不同的事情:
- 寻找一个预先构建的 REST 客户端(即,内置的或第三方框架)。
- 使用 JSON 和内置对象反序列化。
对于这两点,我选择自己实现更多是出于对获取某个/任何东西能够工作的沮丧感,以及想要将精力转移到应用的其余部分,而不是其他原因。我对这类事情的理念是“最好的代码已经由别人编写和测试过了”,我总是犹豫要自己去做别人肯定已经做过的事情。
- 探索响应式扩展 (Reactive Extensions)。我确信异步部分可以使用 Rx 更好地抽象化,这将是下一个应用(无论它是什么)有趣的探索领域。
哦,还有,不要在 `Pivot` 控件上放 `MapControl`。它会让人困惑,而且不好用。
历史
- 2010/12/5 - 首次上传