RegiRide,一个完整的 Windows Phone 应用程序






4.99/5 (51投票s)
一个完整的 WP7 应用程序,集成了 SQL compact 数据库、LINQ to SQL、MVVM Light 和 Dropbox 集成
目录
- 引言
- RegiRide
- 架构
- 工具和框架
- MVVM 和 MVVM Light
- 视图(模型)之间的通信
- 使用设计时数据
- Code first / 数据库模型
- 导出已注册的行程
- LongListSelector
- YLAD(Your Last About Dialog - 你的最后一个关于对话框)
- ListPicker
- 结论
- 接下来呢?
引言
本文描述了 RegiRide 的实现和开发过程。RegiRide 是我为 Windows Phone 开发的第二个应用程序。有关我为何开发 Windows Phone 应用程序的更多信息,可以在此处找到。我的第一篇文章收到了很多积极的反馈,这促使我决定撰写一篇新文章并公开这个应用程序的源代码。我希望这篇文章能激励其他人开始开发新的 Windows Phone 应用程序。我在应用程序实现过程中撰写了本文,并在 RegiRide 通过认证后不久完成了它。RegiRide 目前已在Marketplace上架。
RegiRide
我开发的第二个应用程序名为 RegiRide,顾名思义,它可以用来记录行程。我幸运地能驾驶公司配车。我用这辆车往返于客户之间。在荷兰驾驶公司车辆并将其用于私人用途,您必须支付额外的税款。如果您只将公司车辆用于商业用途,则无需支付这些额外税款。但为了向税务机关证明这一点,您必须创建一份完整的记录,包括车辆的所有行程。
您可以使用纸和笔来创建此记录,这是我在开发 RegiRide 之前使用的方法,或者使用自动化解决方案。RegiRide 是一款 Windows Phone 应用程序,提供了这样的自动化解决方案,它使公司车辆拥有者能够创建其商业行程的记录。
荷兰税务机关为该记录定义了以下要求。
每次行程都需要记录以下信息
- 日期
- 车辆的起始和结束里程
- 起始和结束地址
- 路线(仅当非正常路线时)
- 行程是私人的还是商务的。
此外,我还添加了以下要求
- 通过手机轻松记录行程
- 通过逗号分隔值(CSV)文件导出已注册的行程,以便将数据导入 Excel
- 能够在没有有效数据连接的情况下记录行程
还要求您记录车辆的品牌、型号、车牌号以及使用周期。
我听到您在想,应用商店里不是已经有很多这类应用了吗?嗯,在 Windows Phone 市场上确实存在一些应用程序。但所有这些应用程序都要求您注册一个在线服务来导出数据。对我来说,重要的是数据的归属权和数据本身保留在客户手中。
架构
根据要求,应用程序首先需要一种方法来持久化手机上已注册的行程。幸运的是,Windows Phone Mango 7.5 支持将关系型数据存储在本地数据库中。该数据库存储在应用程序的隔离存储中。本地数据库是 SQL compact 的一个实现,专门用于 Windows Phone Mango。LINQ to SQL 可用,并可用作 ORM 引擎。
下图显示了应用程序的高层架构。

我将从左到右解释架构的每个部分。左侧是应用程序的视图,它根据MVVM 模式数据绑定到视图模型。视图模型通过映射器填充,映射器负责将模型映射到视图模型。存储库负责创建 LINQ to SQL 查询;我为每个实体创建单个存储库。因此,对于 RegiRide,我实现了 RideRepository 和 AddressRepository。
例如,AddressRepository 有一个方法可以从数据库中检索所有地址。
public List<address> GetAll()
{
return
( from address in rideDataContext.Addresses
orderby address.Name
select address
).ToList();
}
此方法返回地址列表,或者根据架构返回模型列表。应用程序使用映射器将模型转换为视图模型。
另一部分功能是将已注册的行程导出到 Dropbox。用户点击视图上的导出按钮,这会触发视图模型中的一个命令。此命令通过存储库从数据库获取所有必要的模型,并将它们映射到导出模型。导出模型列表被转换为 CSV 文件。然后将此文件上传到用户的 Dropbox 帐户。
工具和框架
我对用于开发我上一个应用程序的工具和框架列表进行了一些更改。下面的列表显示了在 RegiRide 开发过程中使用的当前工具和框架列表。
- YLAD(你的最后一个关于对话框)
- MVVM Light(而非 Prism)
- Silverlight Toolkit(一套很棒的免费控件)
- Dropnet(Dropbox 集成)
- Mtiks(分析框架)
- SimpleIoc(包含在 MVVM Light 中的 DI 容器)而非 NFunq
MVVMLight 框架使用表达式而不是字符串来调用 propertychanged
事件,从而定义对属性的引用。这样可以进行编译时检查,而不是正常的运行时检查。我认为这是一个优雅的解决方案。请看 RegiRide 中的示例。它会引入一些轻微的开销,因为 MVVM Light 使用反射来获取属性的实际名称(字符串)。
public AddressViewModel SelectedAddress
{
get
{
return addressViewModel;
}
set
{
if (addressViewModel == value)
{
return;
}
addressViewModel = value;
RaisePropertyChanged(() => SelectedAddress);
}
}
MVVM 和 MVVM Light
正如之前提到的,我在此实现中从Prism切换到了MVVM Light。在本节中,我将更详细地介绍如何从数据库检索数据、更改数据以及将其提交回数据库。从存储库检索的数据通过映射器转换为视图模型。所有这些都会导致例如“管理地址”屏幕的出现。
下面的代码显示了上述视图的模型正在运行。
public class AddressListViewModel : ViewModelBase, INavigable
{
public AddressListViewModel(IAddressRepository addressRepository)
{
this.addressRepository = addressRepository;
this.addNewAddressCommand = new RelayCommand(this.AddNewAddress);
this.loadAddressListCommand = new RelayCommand(this.LoadAddressList);
}
private void LoadAddressList()
{
AddressList = AddressListMapper.Map(
this.addressRepository.GetAllWithNumberOfRides());
}
...
}
loadAddressListCommand 是一个 RelayCommand
,它与实际视图的加载事件进行数据绑定,并在执行时调用 LoadAddressList
方法。此方法使用通过 SimpleIOC 的构造函数注入的 addressRepository
来检索所有地址。AddressListMapper
通过创建 AddressList
(一个 ObservableCollection<AddressViewModel>
)来完成实际工作。我们来更详细地看看 AddressListMapper
中的 Map
方法。
public static ObservableCollection<AddressViewModel> Map(List<Address> addressList)
{
var addresses = new ObservableCollection<AddressViewModel>();
foreach (Address address in addressList)
{
addresses.Add(new AddressViewModel(address, WhichAddress.None));
}
return addresses;
}
Map 方法为每个 Address 实例创建一个 AddressViewModel 实例。实际的模型作为参数传递给构造函数。通过在视图模型中保留对模型的引用,并将更改直接从视图模型写入模型,LINQ to SQL 会跟踪模型的状态。当我们想要更新地址时,只需在数据上下文上调用 SubmitChanges
即可。
public void Save(Address address)
{
rideDataContext.SubmitChanges();
}
新地址的创建和保存也使用相同的方法。当用户添加新地址时,将创建 Address 模型的新实例并使用数据绑定进行填充。我更改了 Save
方法,以便在地址是新实例时调用 InsertOnSubmit
。GetOriginalEntityState
返回一个包含实体原始状态的实例。如果此方法返回 null,则假定它是一个新实体。
public void Save(Address address)
{
if (rideDataContext.Addresses.GetOriginalEntityState(address) == null)
{
rideDataContext.Addresses.InsertOnSubmit(address);
}
rideDataContext.SubmitChanges();
}
有关 LINQ to SQL 以及如何建模和生成数据库的更多信息,请稍后在本文中介绍。
视图(模型)之间的通信
管理地址功能包含两个视图,如下所示。当用户选择一个地址时,将打开详细地址屏幕。当用户按下添加按钮时,用户也会切换到详细屏幕。
在添加或更改这两种情况下,显示列表的视图(模型)与显示地址详细信息的视图(模型)之间都需要进行通信。
该应用程序使用 MVVM Light 框架进行通信。MVVM Light 包含一些使通信更轻松的功能。MVVM Light 框架包含一个消息传递基础结构,允许您在应用程序内部发送消息。基本上有两种发送消息的方式:第一种是使用 RaisePropertyChanged
的重载,第二种是使用 Messenger.Default.Send
命令。对于地址详细屏幕,我使用了 RaisePropertyChanged
的消息传递重载。
public AddressViewModel SelectedAddress
{
set
{
var oldValue = selectedAddress;
selectedAddress = value;
this.RaisePropertyChanged(() => SelectedAddress, oldValue, selectedAddress, true);
NavigationService.Navigate("/Views/AddressDetailView.xaml");
}
get
{
return selectedAddress;
}
}
RaisePropertyChanged
方法包含一个接受布尔参数(称为 broadcast)的重载。RaisePropertyChanged
事件的最后一个参数表示是否应广播有关此更改的消息。MVVM Light 在底层构造并发送一个类型为 PropertyChanged<AddressViewModel>
的消息。因此,任何订阅此特定消息的订阅者都将通过消息传递基础结构收到它。
在另一侧,即详细视图,您必须订阅此特定消息。这可以通过 Messenger 的 Register 方法来实现。在 AddressViewModel
的构造函数中,会针对类型为 PropertyChangedMessage<AddressViewModel>
的消息创建订阅。
public AddressDetailViewModel(IAddressRepository addressRepository,
IRideRepository rideRepository)
{
this.addressRepository = addressRepository;
this.rideRepository = rideRepository;
SaveAddressCommand = new RelayCommand(SaveAddress);
CancelCommand = new RelayCommand(Cancel);
DeleteCommand = new RelayCommand(Delete);
Messenger.Default.Register<PropertyChangedMessage<AddressViewModel>>(
this,
message =>
{
SelectedAddress = null;
SelectedAddress = message.NewValue;
if (message.NewValue != null)
{
whichAddress = message.NewValue.WhichAddress;
}
});
}
整个 AddressViewModel
作为有效负载随消息一起发送,并直接设置到 AddressDetailViewModel
的 SelectedAddress
属性。此属性绑定到视图上的不同字段,视图又会刷新,以便数据直接显示在屏幕上。
通过使用消息而非直接引用进行通信,您可以解耦两个视图模型。在 RegiRide 示例中,列表视图和详细视图彼此的存在互不知情。这将提高视图模型的灵活性和可测试性。
使用设计时数据
Windows Phone 应用程序应根据Metro 设计指南进行设计。Jeff Wilcox 已将其翻译为一系列具体操作和建议。为了确保您的应用程序遵循这些指南,能够在设计时使用数据填充视图非常重要。这也被称为 blendability,即在 Microsoft Blend 中使用设计时数据的能力。可以使用一些指导来在设计时显示(虚拟)数据。有不同的方法可以用来使用设计时数据。可以使用Silverlight 设计时属性,这些属性允许您使用仅在设计时可用的属性。
另一种方法是检查应用程序是否在设计时模式下运行,并基于该信息使用应用程序的不同部分。这是我在实现 RegiRide 时所采取的方法。我使用了一个名为 ContainerLocator 的类,它负责在 SimpleIoc 容器中注册类型。在此类的 configure 方法中,我根据应用程序是运行在设计时还是运行时来切换存储库。
if (!ViewModelBase.IsInDesignModeStatic)
{
SimpleIoc.Default.Register<IAddressRepository, AddressRepository>();
SimpleIoc.Default.Register<IRideRepository, RideRepository>();
SimpleIoc.Default.Register<ISettingsHelper, SettingsHelper>();
}
else
{
SimpleIoc.Default.Register<IAddressRepository, DesignAddressRepository>();
SimpleIoc.Default.Register<IRideRepository, DesignRideRepository>();
SimpleIoc.Default.Register<ISettingsHelper, DesignSettingsHelper>();
}
ViewModelBase
是 MVVM Light 框架中的一个类,顾名思义,它是 ViewModel 可以继承的基类。它还有一个静态属性 IsInDesignModeStatic
,指示应用程序是否在设计时运行。
另一个有用的工具是MetroGridHelper,它根据 Metro 设计指南在视图中显示一个网格。使用此网格可以对齐控件。还可以使用稍作修改的版本在设计时显示网格。
Code first / 数据库模型
当您需要在 Windows Phone 应用程序中持久化数据时,您可以使用 LINQ to SQL 来创建和查询数据库。当使用 LINQ to SQL 框架为 Windows Phone 时,建议的方法是先使用代码。这意味着首先创建您想要存储在数据库中的模型类,并使用 LINQ to SQL 属性来注解这些类。对于 RegiRide,我需要以下数据库模型。
一个行程有一组属性以及与两个地址(起始地址和结束地址)的关系。首先,我尝试先编写类代码,然后从中生成数据库。这有点挑战,因为获取正确的注解很难。
我在www.windowsphonegeek.com上发现了一篇文章,建议使用 SQLMetal。SQLMetal 是一个命令行代码生成工具,能够从现有数据库生成代码,包括映射属性。SQLMetal 工具专门用于完整的 .Net Framework 和LINQ to SQL。因此,生成的代码与 Windows Phone 上的 LINQ to SQL 版本不完全兼容。我所做的是在一个本地 SQL Server 表达版本中创建两个表,并使用 SQLMetal 命令行工具生成类。之后,我删除了不需要或无法编译的部分。下面可以看到生成的已修改的 Address
类的一部分。
[Table]
public class Address : PropertyChangeAndChangingEventHandlerBase
{
private readonly EntitySet<Ride> startRide;
private readonly EntitySet<Ride> endRide;
private Guid id;
private string name;
private string street;
private string postalCode;
private string city;
...
}
EntitySet<Ride>
字段代表与 Ride 的引用,并允许您在使用该字段时自动加载引用的 Ride。对我来说,在这个方面拥有这些字段并不重要。我只需要能够从 Ride 导航到 Address,而不是反过来。但我保留了它,因为它将来可能有用。
数据库创建
数据库由 DataContext
类中的 CreateDatabase
方法创建。该 DataContext
类来自 LINQ to SQL。下面的源代码显示了 RideDataContext
,此类派生自 DataContext 基类,并包含一个 Initialize
方法,该方法在数据库不存在时创建数据库。当用户卸载应用程序时,数据库会自动从手机中删除。
public class RideDataContext : DataContext
{
public Table<Ride> Rides;
public Table<Address> Addresses;
public RideDataContext(string fileOrConnection)
: base(fileOrConnection) { }
public void Initialize()
{
if (!this.DatabaseExists())
{
this.CreateDatabase();
}
}
}
数据上下文的 Initialize
方法在应用程序启动时被调用。此数据上下文使用 SimpleIoc 容器的工厂方法重载进行注册。
SimpleIoc.Default.Register(() =>
{
var context = new RideDataContext(Constants.Settings.DatabaseConnectionString);
context.Initialize();
return context;
});
要检查和浏览 Windows Phone 或模拟器上创建的数据库,我使用了 IsoStoreSpy 工具。此工具允许您查看模拟器或手机的隔离存储,并直接查询或下载数据库。
导出已注册的行程
RegiRide 提供了导出已注册行程的功能。我最初的意图是通过创建电子邮件并将已注册的行程附加到电子邮件中来实现此功能。但令我惊讶的是,我发现目前在 Windows Phone 中没有办法通过附件发送电子邮件。至少没有办法编程实现。

这是一个真正的问题,因为我不想创建一个带有 Web 服务的服务器后端来发送电子邮件。将内容放入电子邮件正文也不是一个选项,因为正文的大小限制为 33k 个字符。在尝试了一些外部电子邮件发送服务后,我最终选择了 Dropbox 集成。感觉像是一种妥协,因为应用程序的用户现在为了导出他们已注册的行程而被迫打开一个 Dropbox 帐户。但另一方面,Dropbox 是一项免费服务,很多人已经在使用了。也许将来我会添加导出行程到其他外部服务的功能,例如 Microsoft SkyDrive 或 Google GDrive。
使用 DropNet 进行 Dropbox 集成
有一些框架为 .Net Framework 提供了 Dropbox 集成,包括 Windows Phone 支持。有适用于 Dropbox 的 Spring.Net Social 扩展,其中包括 Windows Phone 支持,以及同样支持 Windows Phone 的Sharpbox。我最终选择了 Damian Karzon 的DropNet,它开箱即用。
DropNet 包含一个示例 Windows Phone 应用程序,该应用程序运行良好。这样,Dropbox 的集成就很简单了。RideList 视图有一个导出按钮,用于调用 Export
方法将已注册的行程导出到 Dropbox 帐户。
public void Export()
{
if (networkConnection.IsAvailable())
{
if (regiRideSettingsManager.Authenticated && dropNetClient.UserLogin != null && dropNetClient.UserLogin.Token != null)
{
string export = exportManager.GenerateExport(rideRepository.GetAll());
byte[] exportBytes = Encoding.Unicode.GetBytes(export);
string fileName = exportManager.GenerateExportFileName();
dropNetClient.UploadFileAsync(regiRideSettingsManager.DropboxFolder, fileName, exportBytes, UploadSuccess, UploadFailure); }
else
{
MessageBox.Show(AppResources.ExportDropboxNotification);
this.NavigationService.Navigate("/Views/DropboxView.xaml");
}
}
else
{
MessageBox.Show(AppResources.NoNetworkAvailable);
}
}
DropNet 库中的 dropNetClient
负责连接到 Dropbox 帐户。Export
方法检查当前用户是否已认证,如果已认证,则生成 CSV 文件并使用唯一文件名将其上传到用户的 Dropbox。Dropbox 帐户的认证由 DropNet 处理,并使用浏览器控件。
一旦用户允许应用程序访问 Dropbox,访问令牌就会保存在隔离存储中。这样,用户只需授权一次,无需存储用户的凭据。
LongListSelector
为了显示行程和分组,我使用了 Silverlight toolkit 中的 LongListSelector 控件。该控件专门用于显示大量数据,包括分组机制。已注册的行程按周分组。因此,下面截图中的蓝色条表示其下方的行程来自 2012 年第 15 周。这些行程都属于同一组。

这个长列表选择器使用一组模板来自定义其完整的布局。提供以下可自定义模板
- HeaderTemplate、FooterTemplate
- GroupHeaderTemplate
- GroupItemTemplate
- GroupItemsPanel
- ItemsTemplate
这使得长列表选择器成为一个非常可自定义的控件。
该控件不会自动执行分组,您必须自己提供一个分组列表。例如,绑定到一个 ObservableCollection<T> where T : ObservableCollection<T>.
T 类型还必须公开一个 Key 属性,该属性指示分组属性。
在 RegiRide 中,我创建了一个名为 GroupedRideViewModels 的属性,该属性执行分组并绑定到 LongListSelector 的 ItemsSource
属性。
public IEnumerable> GroupedRideViewModels
{
get
{
if (RideList != null)
{
var result = from ride in RideList
group ride by ride.WeekNumberFormatted into grouped
select new Grouping<string, RideViewModel>(grouped);
return result;
}
return null;
}
}
YLAD(Your Last About Dialog - 你的最后一个关于对话框)
在我上一个应用程序中,我创建了自己的关于对话框。这次我使用了 YLAD 组件来创建关于对话框。Peter Kuhn 的这个组件允许您轻松创建关于对话框。它可以通过 XML 文件轻松配置。下图显示了关于对话框。它包含一个枢轴控件,其中包含您的应用程序的版本历史记录。我推荐这个组件,因为它节省了您的时间。

显示对话框只需一行源代码。
private void About()
{
NavigationService.Navigate(
"/YourLastAboutDialog;component/AboutPage.xaml");
}
包含关于对话框的程序集仅在用户选择查看关于对话框时才加载。这将提高应用程序的启动时间。
ListPicker
在创建新行程屏幕中,ListPicker 控件用于让用户选择行程的起始和结束地址。ListPicker 也是 Silverlight Toolkit 的一个控件,类似于 ComboBox 或 DropDownList。它显示选定的项目,并且还可以显示可供选择的项目列表。ListPicker 控件有两种可能的项目选择方式:第一种是就地选择,第二种是全屏弹出窗口,显示可选择的项目。控件会自动在这两者之间切换,具体取决于 ItemCountThreshold
属性中设置的数字。ItemCountThreshold
默认设置为 5。请务必将 ListPicker 包装在 stackpanel 中,这样 ListPicker 在展开时可以增长。
这两种模式都有自己的模板,您可以进行更改。就地选择使用 ItemTemplate
,全屏模式使用 FullModeItemTemplate
。Add Ride 详细屏幕上的两个 ListPicker 都使用相同的地址集合。
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="ListPickerDataItemTemplate">
<toolkit:WrapPanel>
<TextBlock TextWrapping="Wrap" Text="{Binding Model.Name}" />
</toolkit:WrapPanel>
</DataTemplate>
<DataTemplate x:Name="PickerFullModeItemTemplate">
<StackPanel Orientation="Horizontal" Margin="16 21 0 20">
<TextBlock Text="{Binding Model.Name}" Margin="12 0 0 0" FontSize="32" FontFamily="{StaticResource PhoneFontFamilySemiBold}"/>
</StackPanel>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
结论
该应用程序已在marketplace上架,完整源代码可从文章顶部下载。如果您喜欢这篇文章,欢迎投票或评论。谢谢。
接下来呢?
我将继续实现我的下一个应用程序,该应用程序将包含食物、活动瓷砖和与服务器后端的集成。如果您有兴趣,请继续关注我的下一篇文章,以了解另一个完整的 Windows Phone 应用程序。有关每周泰式食谱的新文章已发布。
历史
- v1.0 2012/04/26 首个版本
- v1.1 2012/04/27 小改动,例如链接在新窗口中打开
- v1.2 2012/06/25 添加了新文章的链接。