Windows Phone 7 的 Caliburn Micro
Windows Phone 7 世界中的约定优于配置 MVVM。
引言
Caliburn Micro 框架是首批可用于 Windows Phone 7 (WP7) 的 MVVM 框架之一,由于广泛使用了“约定优于配置”,因此它非常高效。
背景
创建 Windows Phone 7 的 Silverlight 应用程序时,会提供基本的 MVVM 实现,但大多数人会发现这些对于任何非简单的应用程序都不够。我确实建议学习 WP7 Silverlight 编程的人不要使用框架,因为这将使他们更多地接触平台,并让他们更欣赏他们正在使用的框架。
关于 Caliburn 框架
最初的 Caliburn 是作为一个用于 WPF 的开源 (MIT 许可证) MVVM 框架创建的,后来被采用于 Silverlight。在撰写本文时,该项目已达到 2.0 版本。在其项目页面上,您会找到以下消息:
"停止!如果您是新来的,我们建议使用一个具有 90% 功能和强大功能的更简单的框架!看看 Caliburn.Micro"。
此外,“Caliburn”是“Excalibur”的不同拼写/名称之一。
进入 Windows Phone 7
现在,如果您正在为 Silverlight 和 WP7 进行开发,那么选择一个更轻量级的框架是很自然的,如果您也刚开始开发,为什么不选择一个更简单的呢?
以下是有关此框架的历史和创建的一些额外信息:链接。
其他 WP7 MVVM 框架
市面上还有一些其他流行的 MVVM 框架
我们正在解决的问题
我将通过编写一个 WP7 客户端来监控 Cruise Control.Net 的状态,来演示使用 Caliburn Micro 框架的好处。
如果您熟悉 Cruise Control.Net (CC) 的作用,我建议您点击上面的链接,但简而言之,它是一个“自动持续集成服务器,监控您的源代码控制,运行构建、测试和代码分析,帮助您的产品保持成功构建和正确运行的状态”——它是免费的、开源的,并且是用 .NET 编写的。
使用 Caliburn Micro for Windows Phone 7
Caliburn Micro 项目的下载页面包含一个您需要下载和安装的包,该包将安装您所需的二进制文件、示例和 Visual Studio 模板。
在撰写本文时,此项目尚未在 NuGet 上,但我猜测您很快也可以通过这种方式获取它。
更新
2011 年 4 月 20 日:现在有了一个 Caliburn Micro NuGet 包(这里有一篇关于为 Caliburn Micro 添加 NuGet 支持的文章)。
如果您安装了 NuGet 1.2 或更高版本,请从包管理器控制台中输入
Install-Package Caliburn.Micro
空白项目
作为已安装的 Visual Studio 模板的一部分,您会找到这个项目模板
创建项目后,readme.txt 文件将打开,指向 Caliburn Micro 项目页面,该页面目前是查找文档的最佳位置。
项目树看起来会是这样的
注意新增的“Framework”文件夹、“AppBootstrapper
”类以及没有代码隐藏的主页。
“Framework”文件夹包含其下的所有 Caliburn Micro 源代码(没有额外的程序集引用)。
应用程序和应用程序启动器
App.xaml 几乎是空的——它只在资源中引用了 AppBootstrapper
,这是因为该类的所有逻辑都已重定向到 AppBoostrapper
,它将为我们做一些额外的事情。
IoC/依赖注入
作为新项目的一部分,您还会得到一个基本的控制反转容器,但您可以将其替换为任何其他 WP7 容器。要设置容器,AppBootstrapper
在重写的 Configure
方法中包含基本代码。
protected override void Configure()
{
container = new PhoneContainer(this);
container.RegisterPerRequest(typeof(MainPageViewModel),
"MainPageViewModel", typeof(MainPageViewModel));
container.RegisterInstance(typeof(INavigationService),
null, new FrameAdapter(RootFrame));
container.RegisterInstance(typeof(IPhoneService), null,
new PhoneApplicationServiceAdapter(PhoneService));
//container.Activator.InstallChooser<phonenumberchoosertask,>();
//container.Activator.InstallLauncher<emailcomposetask>();
AddCustomConventions();
}
约定优于配置
现在,关于 AddCustomConventions
方法:现代开发往往是一个非常重复且冗长的过程,我们学会了忍受它,这主要是由于 IDE 的巨大改进。XAML 平台 (WPF, WF, Silverlight, WP7) 也不例外。您经常会发现自己重复常见的模式——就像上面这样:
<Button x:Name="Insert"
Content="insert"
IsEnabled="{Binding CanInsert}"
Click="Insert_OnClick"/>
这只是冰山一角。所有这一切都是因为您可以以多种方式设置所有这些属性和绑定,并且由于灵活性增加,我们增加了复杂性。为了应对复杂性,开发人员倾向于使用模式来简化他们对项目的心理图景(如上例——请注意,每行都有“Insert”文本)。为什么不更进一步说:我们将默认使用这些约定,也许我们只需要
<Button x:Name="Insert"
Content="insert"/>
Caliburn Micro 通过使用这些约定以及更多内容为您提供了这种便利。
在上面的示例中,我们必须在页面的代码隐藏中有一个方法,该方法然后会查找 ViewModel 并调用额外的“Insert
”方法。有了 CM,您就可以免费获得所有这些。
- 通过在 View Model 中拥有一个公共的
Insert
方法,CM 将确保“Insert”按钮的点击被传递到 ViewModel 的Insert
方法。 - 如果您有一个返回
bool
的公共方法或一个名为“CanInsert
”的bool
类型属性,CM 将将其绑定到按钮的IsEnabled
属性。
这仅仅是个开始……
是的,但是!
但是,如果您不喜欢已经为您设置好的约定,或者在某些情况下需要覆盖它们怎么办?
- 在 Caliburn Micro 附加到 ViewModel 之前设置的任何绑定和值都不会被更改。这允许您覆盖约定绑定。
- 有一个 API 可以更改约定,您可以在此处查看相关文章:链接。
视图和视图模型的配对
由于在 WP7 中您需要在页面之间导航,因此它们(作为视图)将需要以某种方式查找和初始化适当的视图模型。同样,您很可能会有“AddServerPage.xaml”和“AddServerPageViewModel.cs”作为一对,所以让我们让 Caliburn Micro 为我们配对这些。
container.RegisterPerRequest(typeof(AddServerPageViewModel),
"AddServerPageViewModel", typeof(AddServerPageViewModel));
仅通过这样做,当您导航到 AddServerPage
时,您的 ageView
模型将已准备好并在页面的 DataContext
中等待。
视图模型的基类
可以使用普通的 CLR 对象 (POCO) 作为视图模型。在大多数情况下,您将需要实现 INotifyPropertyChanged
接口。Caliburn Micro 提供了许多视图模型基类供您选择。
PropertyChangedBase
- 是INotifyPropertyChanged
的基本实现,带有一些额外功能。Screen
- 它提供了许多可以挂钩的事件,例如OnActivate
,以及访问已附加视图的选项。Conductor
- 特别是Conductor<t>.Collection.OneActive
,是在处理集合时的预组合视图模型套件。
使用 PropertyChangedBase
让我们深入研究 AddServerPageViewModel
并让它继承自这个基类。
public class AddServerPageViewModel : Framework.PropertyChangedBase
{
public AddServerPageViewModel()
{
_fullAddress = new Uri("http://ccnet.wheelmud.net" +
"/XmlServerReport.aspx");
}
Uri _fullAddress;
public string ServerName
{
get { return _fullAddress.Host; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException();
var ub = new UriBuilder(_fullAddress)
{
Host = value,
};
SetFullAddress(ub.Uri);
}
}
public int PortNumber
{
get { return _fullAddress.Port; }
set
{
var ub = new UriBuilder(_fullAddress)
{
Port = value,
};
SetFullAddress(ub.Uri);
}
}
public string FullAddress
{
get { return _fullAddress.ToString(); }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException();
SetFullAddress(new Uri(value));
}
}
void SetFullAddress(Uri uri)
{
_fullAddress = uri;
CanAddServer = _fullAddress.IsAbsoluteUri;
Refresh();
}
}
我们有三个相互关联的属性,用于编辑服务器 URL。由于这三个属性很可能一起更改,所以我只是调用 PropertyChangedBase
的 Refresh
方法,该方法将通知视图所有这些属性都已更改。很方便。对于视图,我们只需要在 ContentPanel
中添加:
<StackPanel>
<TextBlock><Run Text="Server DNS:"/></TextBlock>
<TextBox x:Name="ServerName" InputScope="Url" />
<TextBlock><Run Text="Server Port:"/></TextBlock>
<TextBox x:Name="PortNumber" InputScope="Number"/>
<TextBlock><Run Text="Full Uri:"/></TextBlock>
<TextBox x:Name="FullAddress" InputScope="Url"/>
</StackPanel>
提示:在模拟器中测试此页面时,不要使用屏幕键盘用鼠标输入,而是可以按计算机键盘上的 Page Up 键来模拟手机的硬件键盘。
使用 Conductor<t>.Collection.OneActive
让您的视图模型继承自这个基类,可以轻松创建用于集合的视图模型,并且在这个变体中,它允许并只维护一个活动的子项。
这个基类有一个 Items
属性,在视图中它会与 List Box 或其他 Items
控件派生类以 x:Name="Items"
配对。活动的项会绑定到 SelectedItem
。
导航问题
您可能已经注意到页面和行为(如果使用 Blend)具有导航到其他页面的能力,但这并不理想,因为我们希望视图模型控制导航。导航到页面后,它可以访问传递的参数,方法是解析查询字符串参数,但同样——这是我们希望视图模型做的事情。
Caliburn Micro 通过公开丰富的 INavigationService
接口并利用依赖注入的强大功能使其易于在视图模型中使用,从而解决了这两个问题。要在视图模型中获取此服务的全部功能,您只需要一个构造函数,它将此接口作为参数。
public AddServerPageViewModel(INavigationService navigationService) {/*…*/}
问题的第二部分(解析传递的参数)会自动处理,这要归功于约定优于配置。如果您导航到的 URI 是“/ProjectPage.xaml?projectId=17”,并且该页面的视图模型中有一个 ProjectId
属性,Caliburn 将自动使用指定的参数值填充 ProjectId
属性。
如果您需要更多地控制视图模型中视图的生命周期,只需继承 Screen
基类或更专业的子类之一。
WP7 不可绑定的应用程序栏
WP7 UI 的一部分是应用程序栏,在其第一个版本中它是不可绑定的。事实上,您需要使用索引来操作按钮,通常直接从您的视图进行操作。
Caliburn Micro for WP7 在此领域进行了重大改进——允许您将应用程序栏绑定到您的视图模型。
您需要做的就是使用 CM 的 AppBarButton
和 AppBarMenuItem
在标准的应用程序栏中,然后通过指定“Message
”属性来识别它需要绑定到哪个目标。
我喜欢在 Blend 中执行此操作,而且幸运的是,您也可以这样做。在 Visual Studio 中右键单击项目以打开您的项目,然后打开您要添加应用程序栏的页面,然后在对象列表中选择该页面。
然后在“属性”的“常用属性”部分。
按“新建”按钮创建应用程序栏。
注意“Buttons (Collection)”并按“...”按钮编辑此应用程序栏中的按钮集合。
现在,“添加另一项”有一个下拉按钮,如果您想要 WP7 框架的按钮,可以使用此下拉按钮,但要使用 Caliburn Micro 改进的按钮版本,只需按“添加另一项”按钮。选择将弹出的“对象”对话框。
我经常发现输入“AppBar”可以快速过滤掉不需要的类型。
从列表中选择 AppBarButton
,然后按 OK 按钮。
添加的项目看起来很像标准按钮。
IconUri
下拉列表允许快速选择图像,包括库存图像,如果需要,这些图像也将添加到项目中。- 显示在按钮下方的文本。
额外的 Message
字段是 Caliburn Micro 的字段,它映射到视图模型中的 Command
。
这不仅会自动映射到“Message”命令(方法),还会映射到控制按钮启用或禁用的“CanMessage
”(方法或属性)。
C# 5 还有很长的路要走
自 Silverlight 2 以来,进行 Web 服务调用的唯一方法是异步的。由于我们有一个多线程环境,并且 Microsoft 通过强制我们仅在 UI 线程上执行所有 UI 活动来阻止我们自断后路,因此在 WP7 中进行网络调用有点痛苦。C# 5 将引入“async”关键字,这将使这个过程更容易,但现在,我们必须在没有它的情况下进行编程。
这带来了一些重要问题
- 如何尽可能多地保留在 MVVM 中,并保持其整洁?
- 如何将所有可能的处理卸载到非 UI 线程,以保持应用程序的性能?
- 如何向用户提供良好的加载/处理反馈,并可能提供取消选项?
对我来说,最棘手的问题之一是:
- 如何从 ViewModel 控制动画以指示忙碌状态?
然后我发现了这篇博文:链接。Caliburn Micro 有一个很棒的功能,即调用以调用命令的方法也可以返回 IEnumerable<IResult>
。Caliburn 将遍历返回的结果,逐个执行它们,直到全部完成、一个取消或抛出/返回错误。这本身就很棒,但还有更多!IResult
接口看起来像这样:
public interface IResult
{
void Execute(ActionExecutionContext context);
event EventHandler<resultcompletioneventargs> Completed;
}
public class ActionExecutionContext
{
public ActionMessage Message;
public FrameworkElement Source;
public object EventArgs;
public object Target;
public DependencyObject View;
public MethodInfo Method;
public Func<bool> CanExecute;
public object this[string key];
}
由于 ActionExecutionContext
中的 Source
属性,我们可以访问实际页面上的对象。这很好,除非您滥用它——所以尽量保持简单,不要过度耦合。我的选择是使用 Visual State Manager 来更改页面的状态。
public IEnumerable<iresult> AddServer()
{
CanAddServer = false;
Refresh();
yield return new SetVisualState("Adding");
var readServer = _readServerFactory();
readServer.Server.Name = ServerName;
readServer.Server.Uri = FullAddress;
yield return readServer;
_serverDataManager.AddServer(readServer.Server);
yield return new SetVisualState("Default");
CanAddServer = true;
_navigationService.GoBack();
}
这个示例来自 AddServerPageViewModel
,它将异步执行这些操作,但对我来说,最棒的部分是我可以轻松而清晰地操作 Visual State。
public class SetVisualState : IResult
{
readonly string _state;
public SetVisualState(string state)
{
_state = state;
}
public void Execute(ActionExecutionContext context)
{
VisualStateManager.GoToState((Control)context.View, _state, true);
Completed(this, new ResultCompletionEventArgs());
}
public event EventHandler<resultcompletioneventargs> Completed = delegate { };
}
结论
Caliburn Micro 是 WP7 的一个很棒的框架,它有望带来巨大的生产力提升,但它仍然存在一些问题,这些问题会让很多人望而却步。
- 它可能会以您可能不习惯的方式重构您的代码。
- 有时您会浪费时间试图弄清楚如何执行一个看起来很直接的操作。
- 与设计时数据和 Caliburn Micro 一起工作是可能的,但那些页面将不得不使用标准的 XAML 绑定。
注释
- 我总是为文本框设置 Input Scope,如果 Visual Studio 没有提供智能感知,Blend 会提供。
- 您的应用程序应该快速启动。除非您确定需要它,否则不要使用启动屏幕。
- 您可以在此处找到一些出色的文章,它们充当 Caliburn Micro 文档:http://caliburnmicro.codeplex.com/documentation。
- 该项目将继续在 http://ccnetwp7.codeplex.com/ 上开发,以添加功能,例如:
- 能够从手机监控和控制构建。
- 实时磁贴 + 服务器通知。
历史
- 2011 年 5 月 31 日 - 初始版本。
- 2011 年 4 月 4 日 - 小幅度清理。