构建 WP7 应用程序的框架






4.90/5 (15投票s)
本文描述了 PhoneCore Framework 的关键实现原则以及如何使用它来构建 Windows Phone 7 应用程序。
目录
- 引言
- 概述
- 配置
- 深入探讨
- 诊断
- 跟踪
- 使用跟踪
- 依赖注入容器
- 面向切面的编程支持
- 引导
- ApplicationBootstrapper 类
- BootsrapperService
- 核心插件
- 自定义插件
- 导航
- 接口
- 实现
- 杂项
- 配置
- 使用示例:SecureBox 应用程序
- 设置框架
- 业务层
- 数据层
- 数据库架构
- 初始化
- 表示层
- 视图
- ViewModel
- 单元测试
- 结论
- 关注点
引言
PhoneCore Framework 是一个帮助在 Windows Phone 7 上构建应用程序的框架。它提供了- 导航引擎,简化了页面间的导航
- 依赖注入容器,帮助创建松耦合的应用程序
- 通过自定义代理类在编译时生成自定义方法拦截器支持面向切面的编程(0.6.1 版本起)
- 配置子系统,允许在不重新构建应用程序的情况下管理工作流
- 跟踪引擎,允许进行事后分析或分析工作流/性能
- 带插件架构的引导引擎
- 基本类型:Lazy, Tuple
更新:自 0.6.1 版本发布以来,该框架通过拦截方法调用提供了面向切面的编程功能子集。AOP 引擎包括
- 通过特殊工具在编译时生成代理,使用配置信息处理实现某些接口的类型
- 这些类型的自定义方法自定义拦截器
容器的更改和改进
- RegisterInstance 方法,注册现有对象实例
- 对象生命周期管理器
- 按名称RegisterXXX/Resolve
- RegisterXXX 方法的流畅界面
这些更改未在此详细描述。请访问官方项目网站获取最新信息
- http://phonecore.codeplex.com/ - 框架项目
- http://securebox.codeplex.com/ - 示例应用程序项目
概述
配置
配置是框架代码的核心,并提供了用于- 控制应用程序执行流程
- 创建松耦合类
- 支持插件架构
- 简化单元测试
<?xml version="1.0" encoding="utf-8" ?> <settings> <system> <container type="PhoneCore.Framework.Container,PhoneCore.Framework"> <!--<types> <type type="" mapTo="" /> </types>--> </container> <!-- bootstrapping --> <bootstrapping> <bootstrappers> <bootstrapper name="Core" type="PhoneCore.Framework.Bootstrap.CoreBootstrapperPlugin, PhoneCore.Framework"> <services> <navigation type="PhoneCore.Framework.Navigation.NavigationService, PhoneCore.Framework" /> <fileSystem type="PhoneCore.Framework.Storage.IsolatedStorageFileService, PhoneCore.Framework" /> <settings type="PhoneCore.Framework.Storage.IsolatedStorageSettingService, PhoneCore.Framework" /> </services> </bootstrapper> <bootstrapper name="Init" type="SecureBox.UI.Infrastructure.Plugins.InitPlugin, SecureBox.UI" /> ... </bootstrappers> </bootstrapping> <navigation> <service /> </navigation> <diagnostic> <!-- trace subsystem--> <traces> <trace name="default" type="PhoneCore.Framework.Diagnostic.Tracing.DefaultTrace, PhoneCore.Framework"> <storage connectionString="Data Source=isostore:/tracedb.sdf"> <init> .... </init> </storage> <level>0</level> <controller type="PhoneCore.Framework.Diagnostic.Tracing.DefaultTraceController, PhoneCore.Framework" template="Resources/Templates/DefaultTraceReport.txt" /> </trace> </traces> </diagnostic> </system> <data> <!-- data storage --> <storage connectionString="Data Source=isostore:/secureboxdb.sdf"> <init> <!-- predefined templates --> <templates> <template name="Password" imageUrl="/Resources/Images/Templates/Password.png" /> <template name="Account" imageUrl="/Resources/Images/Templates/Account.png" /> <template name="Email" imageUrl="/Resources/Images/Templates/Email.png" /> <template name="Note" imageUrl="/Resources/Images/Templates/Note.png" /> </templates> </init> </storage> </data> <views> <!-- page mapping --> <pageMapping> <resolver type="SecureBox.UI.Infrastructure.PageResolver, SecureBox.UI" /> <pages> <page name="Startup"> <uri type="relative" address="/ViewPage/StartupViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.StartupViewPageModel, SecureBox.UI" /> </page> <page name="Library"> <uri type="relative" address="/ViewPage/LibraryViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.LibraryViewPageModel, SecureBox.UI" /> </page> .... </pages> </pageMapping> </views> </settings>如您所见,配置存储在 xml 文件中,其中定义了不同子系统的设置,例如
- system/container – 配置依赖注入容器的设置,属性名称指定使用的容器类型
-
system/bootstrapping - 定义启动服务应仅运行一次的任务,例如
- 在容器中注册类型
- 初始化页面映射
- system/diagnostic/tracing - 定义跟踪子系统
- views/pageMapping – 定义页面及其视图模型之间的映射
深入探讨
目前,配置子系统包含以下类-
ConfigSettings – 配置的入口点。这是一个单例实例
namespace PhoneCore.Framework.Configuration { /// <summary> /// Provides the access to the configuration subsystem /// </summary> public class ConfigSettings { private ConfigElement _root; private ConfigSettings() { var document = XDocument.Load(ConfigFileName); _root = new ConfigElement(document.Root); } private const string ConfigFileName = "application.config"; #region singleton private static object _syncLock = new object(); private volatile static ConfigSettings _instance; public static ConfigSettings Instance { get { if (_instance == null) lock (_syncLock) if (_instance == null) _instance = new ConfigSettings(); return _instance; } } #endregion /// <summary> /// Get section /// </summary> /// <param name="xpath"></param> /// <returns></returns> public ConfigSection GetSection(string xpath) { return (new ConfigSection(_root)).GetSection(xpath); } /// <summary> /// Get the set of sections /// </summary> /// <param name="xpath"></param> /// <returns></returns> public IEnumerable<ConfigSection> GetSections(string xpath) { return (new ConfigSection(_root)).GetSections(xpath); } } }
如上所述,它尝试使用application.config文件作为存储 -
ConfigSection – 封装 xml 节点/属性
namespace PhoneCore.Framework.Configuration { /// <summary> /// Represens a config entry /// </summary> public class ConfigSection { private ConfigElement _element; public ConfigSection(ConfigElement element) { _element = element; } /// <summary> /// Returns the set of ConfigSections /// </summary> /// <param name="xpath"></param> /// <returns></returns> public IEnumerable<ConfigSection> GetSections(string xpath) { return _element.GetElements(xpath).Select(e => new ConfigSection(e)); } /// <summary> /// Returns ConfigSection /// </summary> /// <param name="xpath"></param> /// <returns></returns> public ConfigSection GetSection(string xpath) { return new ConfigSection(new ConfigElement(_element.Node, xpath)); } /// <summary> /// Returns string /// </summary> /// <param name="xpath"></param> /// <returns></returns> public string GetString(string xpath) { return new ConfigElement(_element.Node, xpath).GetString(); } /// <summary> /// Returns int /// </summary> /// <param name="xpath"></param> /// <returns></returns> public int GetInt(string xpath) { ConfigElement element = new ConfigElement(_element.Node, xpath); return new ConfigElement(_element.Node, xpath).GetInt(); } /// <summary> /// Returns type object /// </summary> /// <param name="xpath"></param> /// <returns></returns> public Type GetType(string xpath) { return (new ConfigElement(_element.Node, xpath)).GetType(); } /// <summary> /// Returns the instance of T /// </summary> /// <typeparam name="T"></typeparam> /// <param name="xpath"></param> /// <returns></returns> public T GetInstance<T>(string xpath) { return (T)Activator.CreateInstance(GetType(xpath)); } /// <summary> /// Returns the instance of T /// </summary> /// <typeparam name="T"></typeparam> /// <param name="xpath"></param> /// <param name="args"></param> /// <returns></returns> public T GetInstance<T>(string xpath, params object[] args) { return (T)Activator.CreateInstance(GetType(xpath), args); } } }
公开的成员如下- GetSection/GetSections – 返回一个/多个 ConfigSection GetString – 以字符串形式返回值使用
- GetInt – 以整数形式返回值
- GetType – 以 System.Type 形式返回值
- GetInstance – 以 T 类型实例的形式返回值
-
ConfigElement – 封装 xml 处理操作
namespace PhoneCore.Framework.Configuration { /// <summary> /// Represens a single element of xml /// </summary> public class ConfigElement { private string _xpath; private XElement _node; private XAttribute _attribute; public ConfigElement(XElement root) { _node = root; } public ConfigElement(XElement root, string xpath) { _node = root; _xpath = xpath; Initialize(); } private void Initialize() { string[] paths = _xpath.Split('/'); XElement current = _node; for (int i = 0; i < paths.Length; i++) { if (paths[0].StartsWith("@")) { _attribute = current.Attribute(paths[0].Substring(1)); return; } current = current.Element(paths[i]); } _node = current; } /// <summary> /// Returns the set of elements /// </summary> /// <param name="xpath"></param> /// <returns></returns> public IEnumerable<ConfigElement> GetElements(string xpath) { string[] paths = xpath.Split('/'); int last = paths.Length - 1; XElement current = Node; for (int i = 0; i < last; i++) { current = current.Element(paths[i]); } return current.Elements(paths[last]).Select(x => new ConfigElement(x)); } /// <summary> /// Returns string /// </summary> /// <returns></returns> public string GetString() { if (IsAttribute) return _attribute.Value; if (IsNode) return _node.Value; return null; } /// <summary> /// Returns int /// </summary> /// <returns></returns> public int GetInt() { return int.Parse(GetString()); } /// <summary> /// Returns type /// </summary> /// <returns></returns> public new Type GetType() { string typeName = GetString(); return Type.GetType(typeName); } /// <summary> /// Returns current XElement /// </summary> public XElement Node { get { return _node; } } /// <summary> /// Returns current XAttribute /// </summary> public XAttribute Attribute { get { return _attribute; } } /// <summary> /// true if element represents attribute /// </summary> public bool IsAttribute { get { return _attribute != null; } } /// <summary> /// true if element represents xml node /// </summary> public bool IsNode { get { return _node != null; } } } }
以下是如何使用配置的示例
ConfigSection bootstrappers = ConfigSettings.Instance.GetSection("system/bootstrapping/bootstrappers/bootstrapper "); List<IBootstrapperPlugin> tasks = new List<IBootstrapperPlugin>(); foreach (var bootsrappersSection in bootstrappers) { _trace.Info(_category, String.Format("create '{0}' of type '{1}'", bootsrappersSection.GetString("@name"), bootsrappersSection.GetString("@type"))); var plugin = bootsrappersSection.GetInstance<IBootstrapperPlugin>("@type", bootsrappersSection); tasks.Add(plugin); }
诊断
跟踪
框架的默认诊断子系统允许将所有主要应用程序事件存储到数据库中,并进行事后分析或查看应用程序工作流、性能等。目前,已实现跟踪子系统,以下是最重要的接口-
ITrace – 表示跟踪子系统的跟踪器
namespace PhoneCore.Framework.Diagnostic.Tracing { /// <summary> /// Represents a tracer for tracing subsystem /// </summary> public interface ITrace: IDisposable { /// <summary> /// Level of tracing /// </summary> int Level { get; set; } ... /// <summary> /// Writes record to trace /// </summary> /// <param name="record"></param> void Info(ITraceRecord record); ... /// <summary> /// Writes record to trace /// </summary> /// <param name="record"></param> void Warn(ITraceRecord record); ... /// <summary> /// Writes record to trace /// </summary> /// <param name="record"></param> void Error(ITraceRecord record); ... /// <summary> /// Writes record to trace /// </summary> /// <param name="record"></param> void Fatal(ITraceRecord record); /// <summary> /// Returns the storage of messages /// </summary> /// <returns></returns> object GetUnderlyingStorage(); /// <summary> /// Returns the trace controller /// </summary> /// <returns></returns> ITraceController GetController(); /// <summary> /// Shows whether trace is initialized /// </summary> bool IsInitialized { get; } } }
如您所见,该接口定义了记录不同类型跟踪记录的行为- Info(信息)
- Warn(警告)
- Error(错误)
- Fatal(致命)
-
ITraceCategory – 表示跟踪类别,有助于对跟踪记录进行分组
namespace PhoneCore.Framework.Diagnostic.Tracing { /// <summary> /// Represents a trace category which helps to group trace records /// </summary> public interface ITraceCategory { /// <summary> /// Name of trace category /// </summary> string Name { get; } } }
-
ITraceRecord – 表示单个跟踪记录
namespace PhoneCore.Framework.Diagnostic.Tracing { /// <summary> /// Represents a trace record /// </summary> public interface ITraceRecord { ITraceCategory Category { get; } string Message { get; } DateTime Date { get; } IPage Page { get; } Exception Exception { get;} Type SourceType { get; } } }
使用跟踪
您可以使用默认的跟踪实现,它将跟踪消息记录到本地数据库,/或添加您自己的自定义实现。您application.config文件中的以下部分应定义<diagnostic> <!-- trace subsystem--> <traces> <trace name="default" type="PhoneCore.Framework.Diagnostic.Tracing.DefaultTrace, PhoneCore.Framework"> <storage connectionString="Data Source=isostore:/tracedb.sdf"> <init> <types> <type name="Info" /> <type name="Warn" /> <type name="Error" /> <type name="Fatal" /> </types> </init> </storage> <level>0</level> <controller type="PhoneCore.Framework.Diagnostic.Tracing.DefaultTraceController, PhoneCore.Framework" template="Resources/Templates/DefaultTraceReport.txt" /> </trace> </traces> </diagnostic>PhoneCore 框架将创建此处定义的跟踪列表,如下所示
namespace PhoneCore.Framework.Diagnostic.Tracing { /// <summary> /// Tracer factory /// </summary> public class TraceFactory { public const string Default = "default"; private static readonly Dictionary<string, ITrace> __traces = new Dictionary<string, ITrace>(); private static readonly object __lockInstance = new object(); public static bool IsInitialized { get; private set; } static void Initialize() { try { //get traces var traceConfigs = ConfigSettings.Instance.GetSections("system/diagnostic/traces/trace"); foreach (var traceConfig in traceConfigs) { string name = traceConfig.GetString("@name"); ITrace trace = traceConfig.GetInstance<ITrace>("@type", new object[]{traceConfig}); __traces.Add(name, trace); } IsInitialized = true; } catch(Exception ex) { //Show user message to make clear the reason MessageBox.Show(String.Format("Fatal error: unable to register trace subsystem. Description: {0}", ex)); throw; } } static void Ensure() { if (!IsInitialized) lock (__lockInstance) { if (!IsInitialized) Initialize(); } } /// <summary> /// Get Default tracer /// </summary> /// <returns></returns> public static ITrace GetTrace() { Ensure(); return GetTrace(Default); } /// <summary> /// Gets tracer associated with the given name /// </summary> /// <param name="name"></param> /// <returns></returns> public static ITrace GetTrace(string name) { Ensure(); return __traces[name]; } } }在配置中定义跟踪后,您可以使用TraceFactory类来访问配置的跟踪,该类提供两个静态方法
- GetTrace() – 返回默认跟踪
- GetTrace(string name) – 按名称返回跟踪
private static readonly ITrace _trace = TraceFactory.GetTrace(); private static readonly ITraceCategory _categoryTo = TraceCategory.GetInstance("Navigation.To"); … _trace.Info(_categoryTo, String.Format("Navigate to {0}", pageUri));此代码使用默认跟踪,它使用"Navigation.To"类别写入 Info 消息,该类别将在第一次调用TraceCategory.GetInstance()时自动创建
namespace PhoneCore.Framework.Diagnostic.Tracing { /// <summary> /// Default implementation of ITraceCategory /// </summary> public class TraceCategory : ITraceCategory { private static readonly Dictionary<string, ITraceCategory> __categories = new Dictionary<string, ITraceCategory>(); private static readonly object __lockInstance = new object(); public string Name { get; set; } public TraceCategory(string name) { Name = name; } /// <summary> /// Returns the instance of trace category /// </summary> /// <param name="name"></param> /// <returns></returns> public static ITraceCategory GetInstance(string name) { if (!__categories.ContainsKey(name)) { lock (__lockInstance) { if (!__categories.ContainsKey(name)) __categories.Add(name, new TraceCategory(name)); } } return __categories[name]; } /// <summary> /// Returns registred category names /// </summary> public static IEnumerable<string> GetRegistredCategoryNames { get { lock (__lockInstance) return __categories.Keys; } } } }
以下是跟踪输出示例
依赖注入容器
- RegisterType - 注册接口与实现该接口的类型之间的映射,或仅注册具体实现
- RegisterInstance - 注册实现某个接口的现有实例
- Register - 特殊方法,允许以编程方式定义拦截功能
/// <summary> /// Default implementation of navigation service /// </summary> public class NavigationService : INavigationService { … protected ConfigSection Config { get; private set; } protected IContainer Container { get; private set; } public NavigationService(IConfigSection config, IContainer container) { … PageMapping = container.Resolve<IPageMapping>(); } }
这种方法有利有弊。例如,它通过避免在类的具体实现中使用静态实例和/或依赖项来简化单元测试的创建(参见下面的示例),并简化了扩展/替换框架子系统的能力。但是,它隐藏了服务实际消耗的内容。
面向切面的编程支持
自 0.6.1 版本以来,该框架支持定义可附加到方法的自定义拦截器。如果为被拦截的服务在编译时创建了特殊的代理类(已提供特殊工具),则此操作是可能的
using System.Reflection; using PhoneCore.Framework.IoC.Interception.Proxies; namespace PhoneCore.Framework.Storage { public class FileSystemServiceProxy : ProxyBase, PhoneCore.Framework.Storage.IFileSystemService { public System.IO.Stream OpenFile(System.String path, System.IO.FileMode mode) { var methodInvocation = BuildMethodInvocation(MethodBase.GetCurrentMethod(), path, mode); return RunBehaviors(methodInvocation).GetReturnValue<System.IO.Stream>(); } public void CreateDirectory(System.String path) { var methodInvocation = BuildMethodInvocation(MethodBase.GetCurrentMethod(), path); RunBehaviors(methodInvocation); } public System.Boolean DirectoryExists(System.String path) { var methodInvocation = BuildMethodInvocation(MethodBase.GetCurrentMethod(), path); return RunBehaviors(methodInvocation).GetReturnValue<system.boolean>(); } } }
如您所见,它具有接口方法的存根实现,并继承自ProxyBase类。实现通过特殊的MethodInvocation类型对象运行行为链。此对象包含有关正在执行的真实对象方法的所有必要信息。自定义行为可以验证、记录、分析、禁用等方法的执行。
有两种方法可以将行为附加到已生成代理的类
- 配置
<interception> <behaviors> <behavior name="execute" type="PhoneCore.Framework.IoC.Interception.Behaviors.ExecuteBehavior,PhoneCore.Framework" /> </behaviors> <components> <component interface="PhoneCore.Framework.UnitTests.Stubs.Container.IClassA,PhoneCore.Framework.UnitTests" proxy="PhoneCore.Framework.UnitTests.Stubs.Container.ClassAProxy,PhoneCore.Framework.UnitTests" name ="ClassAProxy"> <behaviors> <clear /> <behavior name="execute" type="PhoneCore.Framework.IoC.Interception.Behaviors.ExecuteBehavior,PhoneCore.Framework" /> <behavior name="trace" type="PhoneCore.Framework.IoC.Interception.Behaviors.TraceBehavior,PhoneCore.Framework" /> </behaviors> </component> <component interface="PhoneCore.Framework.UnitTests.Stubs.Container.IClassB,PhoneCore.Framework.UnitTests" proxy="PhoneCore.Framework.UnitTests.Stubs.Container.ClassBProxy,PhoneCore.Framework.UnitTests" name ="ClassBProxy"> <behaviors> <clear /> <behavior name="profile" type="PhoneCore.Framework.IoC.Interception.Behaviors.ProfileBehavior,PhoneCore.Framework" /> </behaviors> </component>
有默认的execute行为和每个组件的自定义行为。
- 以编程方式
container.Register(Component.For<IClassC>().ImplementedBy<ClassC>() .Proxy<ClassCProxy>() .AddBehavior(new ExecuteBehavior()) .Singleton());
批处理文件的示例,展示了如何使用通过NuGet包或项目源代码提供的特殊工具自动生成代理类
@echo off
pushd
cd /D %~dp0
if '%1'=='' (
Set Mode=Debug
) else (
Set Mode=%1
)
set proxyTargetProject="%~dp0PhoneCore.Framework.UnitTests"
echo %proxyTargetProject%
set doPause=1
if not "%2" == "" set doPause=0
%systemroot%\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe "PhoneCore.Framework.sln" /t:Build /p:Configuration=%Mode%;TargetFrameworkVersion=v4.0
echo run proxy gen utility
PhoneCore.Framework.ProxyGen\bin\%Mode%\PhoneCore.Framework.ProxyGen.exe %proxyTargetProject% %proxyTargetProject%\bin\%Mode% %proxyTargetProject%\Proxies
%systemroot%\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe PhoneCore.Framework.UnitTests\PhoneCore.Framework.UnitTests.csproj /t:Test /p:Configuration=%Mode%;TargetFrameworkVersion=v4.0
@if ERRORLEVEL 1 goto fail
:fail
if "%doPause%" == "1" pause
popd
exit /b 1
:end
popd
if "%doPause%" == "1" pause
exit /b 0
该工具在此处接收以下参数
- 目标项目根文件夹,其中包含application.config
- 程序集位置
- 输出目录
引导
ApplicationBootstrapper 类
引导引擎运行应仅在应用程序启动时执行一次的插件。需要在您的App.xaml中添加以下行<?xml version="1.0" encoding="utf-8"?> <Application … xmlns:vm="clr-namespace:SecureBox.UI.ViewModel" xmlns:core="clr-namespace:PhoneCore.Framework.Bootstrap;assembly=PhoneCore.Framework" mc:Ignorable="d"> <!--Application Resources--> <Application.Resources> <core:ApplicationBootstrapper x:Key="Bootstrapper" d:IsDataSource="False" /> .. </Application.Resources> … </Application>ApplicationBootstrapper类执行以下操作
- 创建容器
- 创建启动服务
- 运行启动服务
BootsrapperService 类
BootsrapperService运行已定义的插件列表namespace PhoneCore.Framework.Bootstrap { /// <summary> /// Provides default functionality to execute startup plugins /// </summary> public class BootstrapperService: IBootstrapperService { //TODO ensure that it runs only once private readonly ITrace _trace = TraceFactory.GetTrace(); private readonly ITraceCategory _category = TraceCategory.GetInstance("Bootstrapping.Service"); private readonly IBootstrapperPlugin[] _plugins; public BootstrapperService(ConfigSection config, IContainer container) { _trace.Info(_category, "get bootstrappers from configuration"); var bootstrappers = config.GetSections("bootstrappers/bootstrapper"); List<IBootstrapperPlugin> tasks = new List<IBootstrapperPlugin>(); foreach (var bootsrappersSection in bootstrappers) { _trace.Info(_category, String.Format("create '{0}' of type '{1}'", bootsrappersSection.GetString("@name"), bootsrappersSection.GetString("@type"))); var plugin = bootsrappersSection.GetInstance<IBootstrapperPlugin>("@type", bootsrappersSection, container); tasks.Add(plugin); } _plugins = tasks.ToArray(); _trace.Info(_category, String.Format("register plugins: {0}", _plugins.Count())); } public BootstrapperService(IBootstrapperPlugin[] plugins) { _trace.Info(_category, String.Format("register plugins: {0}", plugins.Count())); _plugins = plugins; } #region IBootstrapperService members /// <summary> /// Run all registred bootstrappers /// </summary> /// <returns></returns> public bool Run() { _trace.Info(_category, "run bootstrapper"); return _plugins.Aggregate(true, (current, task) => current & task.Run()); } /// <summary> /// Updates all registred bootstrappers /// </summary> /// <returns></returns> public bool Update() { return _plugins.Aggregate(true, (current, task) => current & task.Update()); } /// <summary> /// Returns the task registered in bootstrapper by name /// </summary> /// <param name="name"></param> /// <returns></returns> public IBootstrapperPlugin GetPlugin(string name) { return _plugins.Single(task => task.Name == name); } /// <summary> /// Returns the task registered in bootstrapper by type /// </summary> /// <param name="t"></param> /// <returns></returns> public IBootstrapperPlugin GetPlugin(Type t) { return _plugins.Single(task => task.GetType() == t); } /// <summary> /// Returns the task registered in bootstrapper by type /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public IBootstrapperPlugin GetPlugin<T>() { return GetPlugin(typeof(T)); } #endregion } }以下定义了以下方法
- Run() – 通过调用每个插件的 Run() 方法来执行所有插件
- Update() – 调用每个插件的Update()方法
- GetPlugin() – 通过类型或名称获取已注册的插件实例
container.Resolve<IBootstrapperService>().GetPlugin<CoreBootstrapperPlugin>();让我们看看主要的插件:CoreBootstrapperPlugin
核心插件
CoreBootstrapperPlugin插件在容器中注册核心服务namespace PhoneCore.Framework.Bootstrap { /// <summary> /// Registers core services, should be executed first /// </summary> public class CoreBootstrapperPlugin: BootstrapperPlugin { public CoreBootstrapperPlugin(ConfigSection config, IContainer container) : base(config, container) { } /// <summary> /// Registers core services /// </summary> /// <returns></returns> public override bool Run() { try { Trace.Info(Category, "register page mapping"); Container.Register<PageMapping>(ConfigSettings.Instance.GetSection("views/pageMapping"), Container); Trace.Info(Category, "register navigation service"); var navigationSection = Config.GetSection("services/navigation"); Container.Register(typeof (INavigationService), navigationSection.GetType("@type"), navigationSection, Container); Trace.Info(Category, "register file system service"); var fileSection = Config.GetSection("services/fileSystem"); Container.Register(typeof(IFileSystemService), fileSection.GetType("@type"), fileSection, Container); Trace.Info(Category, "register settings service"); var settingSection = Config.GetSection("services/settings"); Container.Register(typeof(ISettingService), settingSection.GetType("@type"), settingSection, Container); return true; } catch (Exception ex) { Trace.Fatal(Category, "registration failed", ex); throw; } } /// <summary> /// Updates /// </summary> /// <returns></returns> public override bool Update() { //not updatable Trace.Info(Category, "update"); return false; } } }服务应在配置中定义并实现框架的接口
<bootstrapper name="Core" type="PhoneCore.Framework.Bootstrap.CoreBootstrapperPlugin, PhoneCore.Framework"> <services> <navigation type="PhoneCore.Framework.Navigation.NavigationService, PhoneCore.Framework" /> <fileSystem type="PhoneCore.Framework.Storage.IsolatedStorageFileService, PhoneCore.Framework" /> <settings type="PhoneCore.Framework.Storage.IsolatedStorageSettingService, PhoneCore.Framework" /> </services> </bootstrapper>之后,用户代码可以消费已注册的服务。它们由核心插件在容器中注册
- PageMapping
- NavigationService
- FileSystemService
- SettingsService
- IsolatedStorageSettingsService
自定义插件
您可以创建自定义插件,这些插件将在Core插件之后执行。您需要做的就是创建一个继承自IBootstrapperPlugin的类namespace PhoneCore.Framework.Bootstrap { /// <summary> /// Represents a startup plugin /// </summary> public interface IBootstrapperPlugin { /// <summary> /// The name of plugin /// </summary> string Name { get; } /// <summary> /// Run plugin /// </summary> /// <returns></returns> bool Run(); /// <summary> /// Update plugin /// </summary> /// <returns></returns> bool Update(); } }并在配置节中添加它
<system> <!-- bootstrapping --> <bootstrapping> <bootstrappers> <bootstrapper name="Core" type="PhoneCore.Framework.Bootstrap.CoreBootstrapperPlugin, PhoneCore.Framework" /> <bootstrapper name="Init" type="SecureBox.UI.Infrastructure.Plugins.InitPlugin, SecureBox.UI" /> …注意:顺序很重要。核心插件必须放在第一个,因为插件是按定义的顺序执行的。
导航
接口
导航引擎简化了应用程序页面之间的导航方式。它提供了以下接口-
IPage – 表示由映射用于导航的页面类型
namespace PhoneCore.Framework.Navigation { /// <summary> /// Represetns a type of page which is used by navigation subsystem /// </summary> public interface IPage: IEquatable<IPage> { string Name { get; } } }
-
INavigationService – 定义导航引擎的行为
namespace PhoneCore.Framework.Navigation { /// <summary> /// Represents navigation service /// </summary> public interface INavigationService { /// <summary> /// Navigating event /// </summary> event NavigatingCancelEventHandler Navigating; /// <summary> /// Navigates to uri /// </summary> /// <param name="pageUri"></param> void NavigateTo(Uri pageUri); /// <summary> /// Navigates to uri and passes parameters /// </summary> /// <param name="pageUri"></param> /// <param name="parameters"></param> void NavigateTo(Uri pageUri, Dictionary<string, object> parameters); /// <summary> /// Navigates using page instance in mapping /// </summary> /// <param name="type"></param> void NavigateTo(IPage type); /// <summary> /// /// </summary> /// <param name="type"></param> /// <param name="parameters"></param> void NavigateTo(IPage type, Dictionary<string, object> parameters); /// <summary> /// Goes back /// </summary> void GoBack(); } }
-
IPageResolver – 实现此接口的类应根据其类型返回相应的页面
namespace PhoneCore.Framework.Navigation { public interface IPageResolver { IPage Resolve(string name); } }
实现
导航的关键部分是通过配置定义的页面映射<views> <!-- page mapping --> <pageMapping> <resolver type="SecureBox.UI.Infrastructure.PageResolver, SecureBox.UI" /> <pages> <page name="Startup"> <uri type="relative" address="/ViewPage/StartupViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.StartupViewPageModel, SecureBox.UI" /> </page> <page name="Library"> <uri type="relative" address="/ViewPage/LibraryViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.LibraryViewPageModel, SecureBox.UI" /> </page> <page name="Password"> <uri type="relative" address="/ViewPage/PasswordViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.PasswordViewPageModel, SecureBox.UI" /> </page> <page name="Account"> <uri type="relative" address="/ViewPage/AccountViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.AccountViewPageModel, SecureBox.UI" /> </page> <page name="Email"> <uri type="relative" address="/ViewPage/EmailViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.EmailViewPageModel, SecureBox.UI" /> </page>如您所见,每个页面都由 page 节点定义
- name – 页面名称,唯一。解析器应通过此名称返回页面实例
- uri – 定义到此页面的路径
- viewModel – 页面的 ViewModel 类型。它应实现 PhoneCore.Framework.Views.IViewModel 接口
namespace PhoneCore.Framework.Navigation { /// <summary> /// Represents page mapping /// </summary> public class PageMapping { private readonly object _syncLock = new object(); private Dictionary<IPage, Tuple<Uri, Lazy<IViewModel>>> _pageMapping = new Dictionary<IPage, Tuple<Uri, Lazy<IViewModel>>>(); public PageMapping(ConfigSection config, IContainer container) { var pages = config.GetSections("pages/page"); var resolverConfig = config.GetSection("resolver"); IPageResolver resolver = resolverConfig.GetInstance<IPageResolver>("@type", resolverConfig, container); foreach (var configPage in pages) { var name = configPage.GetString("@name"); var page = resolver.Resolve(name); //TODO support other types of uri var uri = new Uri(configPage.GetSection("uri").GetString("@address"), UriKind.Relative); var vmSection = configPage.GetSection("viewModel"); lock(_syncLock) _pageMapping.Add(page, new Tuple<Uri, Lazy<IViewModel>>( uri, new Lazy<IViewModel>(() => vmSection.GetInstance<IViewModel>("@type", vmSection, container)))); } } public PageMapping(Dictionary<IPage, Tuple<Uri, Lazy<IViewModel>>> mapping) { _pageMapping = mapping; } /// <summary> /// get page uri by its type /// </summary> /// <param name="page"></param> /// <returns></returns> public Uri GetUri(IPage page) { return _pageMapping.Single(m => m.Key.Equals(page)).Value.Item1; } /// <summary> /// get page's ViewModel by its type /// </summary> /// <param name="page"></param> /// <returns></returns> public IViewModel GetViewModel(IPage page) { return _pageMapping.Single(m => m.Key.Equals(page)).Value.Item2.Value; } /// <summary> /// get page type by uri /// </summary> /// <param name="pageUri"></param> /// <returns></returns> public IPage GetPage(Uri pageUri) { return _pageMapping.Keys.Single(m => _pageMapping[m].Item1 == pageUri); } } }因此,构造函数创建一个字典Dictionary <IPage, Tuple<Uri, Lazy<IViewModel>>>,其中键是页面类型,值是包含IViewModel“延迟”包装器的元组。这可以防止在PageMapping初始化期间创建所有 ViewModel。
杂项
还有一些我尚未描述的类。简而言之-
基本类型
- Lazy – 提供“延迟”类型的实现。PageMapping 类已在使用它
- Tuple – 元组类
-
PhoneCore.Framework.Views.Views 命名空间
- IViewMode– 定义 ViewModel 的行为
- ViewPage – 基类,用于替代内置的
使用示例:SecureBox 应用程序
SecureBox 是一个Windows Phone 7.5应用程序,它允许存储您的敏感信息,如帐户凭据、电话号码、密码,并防止访问。目前开发尚未完成,但其代码可以帮助理解使用 PhoneCore Framework 的主要优势。
让我们专注于关键概念。
设置框架
首先定义application.config<?xml version="1.0" encoding="utf-8" ?> <settings> <system> <container type="PhoneCore.Framework.Container,PhoneCore.Framework"> <!--<types> <type type="" mapTo="" /> </types>--> </container> <!-- bootstrapping --> <bootstrapping> <bootstrappers> <bootstrapper name="Core" type="PhoneCore.Framework.Bootstrap.CoreBootstrapperPlugin, PhoneCore.Framework"> <services> <navigation type="PhoneCore.Framework.Navigation.NavigationService, PhoneCore.Framework" /> <fileSystem type="PhoneCore.Framework.Storage.IsolatedStorageFileService, PhoneCore.Framework" /> <settings type="PhoneCore.Framework.Storage.IsolatedStorageSettingService, PhoneCore.Framework" /> </services> </bootstrapper> <bootstrapper name="Init" type="SecureBox.UI.Infrastructure.Plugins.InitPlugin, SecureBox.UI" /> <bootstrapper name="DataContext" type="SecureBox.UI.Infrastructure.Plugins.DataContextPlugin, SecureBox.UI" /> <bootstrapper name="PassGen" type="SecureBox.UI.Infrastructure.Plugins.PassGenPlugin, SecureBox.UI" /> </bootstrappers> </bootstrapping> <navigation> <service /> </navigation> <diagnostic> <!-- trace subsystem--> <traces> <trace name="default" type="PhoneCore.Framework.Diagnostic.Tracing.DefaultTrace, PhoneCore.Framework"> <storage connectionString="Data Source=isostore:/tracedb.sdf"> <init> <types> <type name="Info" /> <type name="Warn" /> <type name="Error" /> <type name="Fatal" /> </types> </init> </storage> <level>0</level> <controller type="PhoneCore.Framework.Diagnostic.Tracing.DefaultTraceController, PhoneCore.Framework" template="Resources/Templates/DefaultTraceReport.txt" /> </trace> </traces> </diagnostic> </system> <data> <!-- data storage --> <storage connectionString="Data Source=isostore:/secureboxdb.sdf"> <init> <!-- predefined templates --> <templates> <template name="Password" imageUrl="/Resources/Images/Templates/Password.png" /> <template name="Account" imageUrl="/Resources/Images/Templates/Account.png" /> <template name="Email" imageUrl="/Resources/Images/Templates/Email.png" /> <template name="Note" imageUrl="/Resources/Images/Templates/Note.png" /> </templates> </init> </storage> </data> <views> <!-- page mapping --> <pageMapping> <resolver type="SecureBox.UI.Infrastructure.PageResolver, SecureBox.UI" /> <pages> <page name="Startup"> <uri type="relative" address="/ViewPage/StartupViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.StartupViewPageModel, SecureBox.UI" /> </page> <page name="Library"> <uri type="relative" address="/ViewPage/LibraryViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.LibraryViewPageModel, SecureBox.UI" /> </page> <page name="Password"> <uri type="relative" address="/ViewPage/PasswordViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.PasswordViewPageModel, SecureBox.UI" /> </page> <page name="Account"> <uri type="relative" address="/ViewPage/AccountViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.AccountViewPageModel, SecureBox.UI" /> </page> <page name="Email"> <uri type="relative" address="/ViewPage/EmailViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.EmailViewPageModel, SecureBox.UI" /> </page> <page name="Note"> <uri type="relative" address="/ViewPage/NoteViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.NoteViewPageModel, SecureBox.UI" /> </page> <page name="Search"> <uri type="relative" address="/ViewPage/SearchViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.SearchViewPageModel, SecureBox.UI" /> </page> <page name="Settings"> <uri type="relative" address="/ViewPage/SettingsViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.SettingsViewPageModel, SecureBox.UI" /> </page> <page name="Health"> <uri type="relative" address="/ViewPage/HealthViewPage.xaml" /> <viewModel type="SecureBox.UI.ViewModel.HealthViewPageModel, SecureBox.UI" /> </page> </pages> </pageMapping> </views> </settings>然后将标准的 MVVMLightViewLocator类更改为
/// <summary> /// This class contains static references to all the view models in the /// application and provides an entry point for the bindings. /// </summary> public class ViewModelLocator { /// <summary> /// Initializes a new instance of the ViewModelLocator class. /// </summary> public ViewModelLocator() { } private PageMapping _pageMapping; protected PageMapping PageMapping { get { if(_pageMapping == null) { var bootstrapper = Application.Current.Resources["Bootstrapper"] as ApplicationBootstrapper; IContainer container = bootstrapper.GetContainer(); _pageMapping = container.Resolve<PageMapping>(); } return _pageMapping; } } #region ViewModel properties /// <summary> /// Gets the Startup property which defines the main viewmodel. /// </summary> [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This non-static member is needed for data binding purposes.")] public IViewModel Startup { get { return PageMapping.GetViewModel(Page.Get(Page.EnumType.Startup));//_startup; } } … }之后,将ApplicationBootstrapper定义为 App.xaml 中的应用程序资源
<?xml version="1.0" encoding="utf-8"?> <Application x:Class="SecureBox.UI.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:SecureBox.UI.ViewModel" xmlns:core="clr-namespace:PhoneCore.Framework.Bootstrap;assembly=PhoneCore.Framework" mc:Ignorable="d"> <!--Application Resources--> <Application.Resources> <core:ApplicationBootstrapper x:Key="Bootstrapper" d:IsDataSource="False" /> <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" /> </Application.Resources> <Application.ApplicationLifetimeObjects> <!--Required object that handles lifetime events for the application--> <shell:PhoneApplicationService Launching="Application_Launching" Closing="Application_Closing" Activated="Application_Activated" Deactivated="Application_Deactivated" /> </Application.ApplicationLifetimeObjects> </Application>进行这些更改后,应用程序启动器将自动执行。让我们关注 SecureBox 的设计和代码。
业务层
域实体由仅包含公共属性的简单类表示。这些类被称为纯粹的旧 CLR 对象(POCO):
有
- Record – 在单个实体中存储用户定义的数据
-
Template – 代表用户记录的通用模板。有以下内置模板
- 密码
- Account
- 电子邮件
- 注意
- Keyword – 代表Record的类别。它允许将记录绑定到不同的类别。这有助于在集合中搜索它
- Property – 代表记录的单个字段,用于存储用户数据单元
- 创建
- 编辑
- 删除(尚未实现)
数据层
数据库架构
用于存储业务实体的数据库具有以下架构
可以使用不同的企业模式来访问数据库中存储的数据
- 存储库
- 工作单元
- 事务脚本
sqlmetal c:\temp\ secureboxdb.sdf /code:"c:\temp\ secureboxdb.cs" /language:csharp /namespace: SecureBox.UI.Infrastructure.Data /context: SecureBoxDataContext /pluralize以下是结果示例
[Table(Name="Keywords")] public partial class Keyword : INotifyPropertyChanging, INotifyPropertyChanged { … [Table(Name="Properties")] public partial class Property : INotifyPropertyChanging, INotifyPropertyChanged { … [Table(Name="Records")] public partial class Record : INotifyPropertyChanging, INotifyPropertyChanged { … [Table(Name="RecordKeywords")] public partial class RecordKeyword : INotifyPropertyChanging, INotifyPropertyChanged { … [Table(Name="Templates")] public partial class Template : INotifyPropertyChanging, INotifyPropertyChanged {该命令还将生成一个继承自System.Data.Linq.DataContext的类,这是“工作单元”模式的示例
[System.Data.Linq.Mapping.DatabaseAttribute(Name="secureboxdb")] public partial class SecureBoxDataContext : System.Data.Linq.DataContext { …对于域逻辑,我决定使用Repository模式,并将数据处理封装到单个类中,该类在容器中存储为IDataContextService
namespace SecureBox.UI.Infrastructure.Data { /// <summary> /// A gateway to data layer /// </summary> public interface IDataContextService: IDisposable { IRepository<Model.Template> Templates { get; } IRepository<Model.Record> Records { get; } IRepository<Model.Keyword> Keywords { get; } IRepository<Model.Property> Properties { get; } event EventHandler<EventArgs> OnChanged; void Delete(); void Commit(); } }如您所见,它暴露了与我们的域层相关的实体,而不是数据层
初始化
初始化和注册处理数据的代码的正确方法是自定义启动插件namespace SecureBox.UI.Infrastructure.Plugins { /// <summary> /// Initializes DataContextService /// </summary> public class DataContextPlugin : BootstrapperPlugin { public DataContextPlugin(ConfigSection config, IContainer container) : base(config, container) { } public override bool Run() { try { Trace.Info(Category, "register data context instance"); //get connection string string connectionString = ConfigSettings.Instance.GetSection("data/storage").GetString("@connectionString"); //create DataContext instance SecureBoxDataContext dataContext = new SecureBoxDataContext(connectionString); //register data context service Container.Register<IDataContextService, DataContextService>(dataContext); //check whether database exists if(dataContext.DatabaseExists()) dataContext.DeleteDatabase(); if (!dataContext.DatabaseExists()) { // dataContext.DeleteDatabase(); // } CreateDatabase(dataContext); } return true; } catch (Exception ex) { Trace.Fatal(Category, "run is failed", ex); throw; } }
这是始终运行一次的代码
/// <summary> /// Creates dataBase /// </summary> /// <param name="dataContext"></param> private void CreateDatabase(SecureBoxDataContext dataContext) { Trace.Info(Category, "create database"); dataContext.CreateDatabase(); IDataContextService dataContextService = Container.Resolve<IDataContextService>(); Trace.Info(Category, "insert templates from configuration"); var templates = ConfigSettings.Instance.GetSections("data/storage/init/templates/template"); foreach (var templateConfig in templates) { var name = templateConfig.GetString("@name"); var imageUrl = templateConfig.GetString("@imageUrl"); dataContextService.Templates.Add(new Template() {Name = name, ImageUrl = imageUrl}); } dataContextService.Commit(); }
表示层
视图
用户界面通过使用 MVVM 模式分离标记和代码来构建。我为此使用了MVVMLight 框架namespace SecureBox.UI.ViewModel { /// <summary> /// Provides ViewModel's base functionaluty /// </summary> public class ViewModelBase : GalaSoft.MvvmLight.ViewModelBase, IViewModel { protected INavigationService NavigationService { get; set; } protected ISettingService SettingService { get; set; } protected ConfigSection Config { get; private set; } protected IContainer Container { get; private set; } public ViewModelBase(ConfigSection config, IContainer container): base() { Config = config; Container = container; NavigationService = Container.Resolve<INavigationService>(); SettingService = Container.Resolve<ISettingService>(); } private Dictionary<string, object> _navigationParameters = null; public Dictionary<string, object> NavigationParameters { get { return _navigationParameters; } set { _navigationParameters = value; ReadNavigationParameters(); } } protected virtual void ReadNavigationParameters() { } /// <summary> /// Saves view model state into dictionary /// </summary> /// <param name="state"></param> public virtual void SaveStateTo(IDictionary<string, object> state) { } /// <summary> /// Restores view model state from dictionary /// </summary> /// <param name="state"></param> public virtual void LoadStateFrom(IDictionary<string, object> state) { } } }这是实现 PhoneCore 接口并继承自 MVVMLight 基类的默认视图模型类。这种方法允许我们使用诸如RaisePropertyChanged之类的有用方法。让我们看一下单个模板:Account。这是标记
<Views:ViewPage x:Class="SecureBox.UI.ViewPage.AccountViewPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit" xmlns:custom="clr-namespace:SecureBox.Controls;assembly=SecureBox.Controls" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP71" xmlns:Views="clr-namespace:PhoneCore.Framework.Views;assembly=PhoneCore.Framework" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="480" DataContext="{Binding Account, Source={StaticResource Locator}}" shell:SystemTray.IsVisible="False"> <Grid x:Name="LayoutRoot"> <Grid.Background> <ImageBrush ImageSource="/Resources/Images/LibraryBackground.jpg"/> </Grid.Background> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock x:Name="ApplicationTitle" Text="SECURE BOX" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="PageTitle" Text="account" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/> </StackPanel> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <ScrollViewer> <StackPanel Orientation="Vertical"> <!-- name --> <TextBlock Text="Name" Foreground="{StaticResource PhoneSubtleBrush}" Margin="12,0,0,0"/> <toolkit:PhoneTextBox Hint="name of your password in library" Text="{Binding Name, Mode=TwoWay}" MaxLength="64" LengthIndicatorVisible="True" InputScope="Text"/> <!-- keywords --> <TextBlock Text="Keywords" Foreground="{StaticResource PhoneSubtleBrush}" Margin="12,-24,0,0"/> <toolkit:PhoneTextBox Hint="keywords for searching" Text="{Binding Keyword, Mode=TwoWay}" MaxLength="64" LengthIndicatorVisible="True" InputScope="Text"/> <!-- account name --> <TextBlock Text="Account name" Foreground="{StaticResource PhoneSubtleBrush}" Margin="12,-24,0,0"/> <toolkit:PhoneTextBox Hint="your account name/login" Text="{Binding AccountName, Mode=TwoWay}" MaxLength="64" LengthIndicatorVisible="True" InputScope="Text"/> <!-- password --> <TextBlock Text="Password" Foreground="{StaticResource PhoneSubtleBrush}" Margin="12,-24,0,0"/> <toolkit:PhoneTextBox Hint="your password" Text="{Binding Password, Mode=TwoWay}" ActionIcon="/Resources/Images/Actions/Refresh.png" MaxLength="200" LengthIndicatorVisible="True" InputScope="Text"> <i:Interaction.Triggers> <i:EventTrigger EventName="ActionIconTapped"> <cmd:EventToCommand Command="{Binding PasswordActionIconTappedCommand}" /> </i:EventTrigger> </i:Interaction.Triggers> </toolkit:PhoneTextBox> <!-- email name --> <TextBlock Text="Email" Foreground="{StaticResource PhoneSubtleBrush}" Margin="12,-24,0,0"/> <toolkit:PhoneTextBox Hint="email related to account" Text="{Binding Email, Mode=TwoWay}" MaxLength="64" LengthIndicatorVisible="True" InputScope="EmailNameOrAddress"/> <!-- description name --> <TextBlock Text="Description" Foreground="{StaticResource PhoneSubtleBrush}" Margin="12,-24,0,0"/> <toolkit:PhoneTextBox Hint="some description" Text="{Binding Description, Mode=TwoWay}" MaxLength="500" MinHeight="240" TextWrapping="Wrap" AcceptsReturn="True" LengthIndicatorVisible="True" InputScope="Text"/> </StackPanel> </ScrollViewer> </Grid> <custom:BindableApplicationBar x:Name="AppBar" BarOpacity="0.2"> <custom:BindableApplicationBarIconButton Command="{Binding SaveCommand}" IconUri="/Resources/Images/AppBar/appbar.save.png" Text="Save" /> <custom:BindableApplicationBarIconButton Command="{Binding CancelCommand}" IconUri="/Resources/Images/AppBar/appbar.cancel.png" Text="Cancel" /> </custom:BindableApplicationBar> </Grid> </Views:ViewPage>正如您可能注意到的,这里我使用的是Silverlight 工具包。您应该看一下
- root tag - 我切换到使用 PhoneCore 框架页面
- data context 设置为使用我们的 ViewModel 定位器类
ViewModel
以下是页面的视图模型namespace SecureBox.UI.ViewModel { /// <summary> /// Account template view model /// </summary> public class AccountViewPageModel: TemplateViewPage { public AccountViewPageModel(ConfigSection config, IContainer container) : base(config, container) { PasswordActionIconTappedCommand = new RelayCommand(OnGeneratePassword); } #region Methods /// <summary> /// Sets new password to Password input /// </summary> private void OnGeneratePassword() { //TODO extend implementation Password = Container.Resolve<IPasswordGenerator>().Generate(); RaisePropertyChanged("Password"); } #endregion /// <summary> /// Returns the properites of template /// </summary> /// <returns></returns> protected override IEnumerable<Property> GetProperties() { return new [] { new Property() {Name = "AccountName", Value = AccountName}, new Property() {Name = "Password", Value = Password}, new Property() {Name = "Email", Value = Email}, new Property() {Name = "Description", Value = Description}, }; } protected override string GetTemplateName() { return "Account"; } protected override bool IsValid() { return base.IsValid(); } protected override void FillProperties() { Name = EditRecord.Name; Keyword = GetKeywordsField(); AccountName = GetPropertyValueByName("AccountName"); Password = GetPropertyValueByName("Password"); Email = GetPropertyValueByName("Email"); Description = GetPropertyValueByName("Description"); } protected override void EraseProperties() { Name = string.Empty; Keyword = string.Empty; AccountName = string.Empty; Password = string.Empty; Email = string.Empty; Description = string.Empty; } #region Binding properties public string AccountName { get; set; } public string Password { get; set; } public string Email { get; set; } public string Description { get; set; } #endregion #region Command properties public RelayCommand PasswordActionIconTappedCommand { get; private set; } #endregion } }在我看来,实现很简单:它继承自所有模板的基类,这些模板继承了我之前提到的 ViewModelBase。
public abstract class TemplateViewPage: ViewModelBase { protected readonly IDataContextService dataContextService; … public TemplateViewPage(ConfigSection config, IContainer container) : base(config, container) { dataContextService = Container.Resolve<IDataContextService>(); SaveCommand = new RelayCommand(OnSaveCommand); CancelCommand = new RelayCommand(OnCancelCommand); }让我们更仔细地研究视图模型的单元测试。
单元测试
PhoneCore Framework 试图避免使用静态类或方法,因为它们会干扰创建可测试的代码。相反,依赖注入容器被传递到框架的所有组件中。依赖注入和 MVVM 模式有助于为您的ViewModel类创建可测试的代码。这是检查我们是否可以创建记录的测试。它模拟了帐户模板的 UI 控件的工作流namespace SecureBox.UI.UnitTests.ViewModel { [TestClass] public class AccountViewPageModelTests { [TestMethod] public void CanSave() { //arrange var configContainer = ConfigSettings.Instance.GetSection("system/container"); Container container = new Container(configContainer); IBootstrapperService service = new BootstrapperService( ConfigSettings.Instance.GetSection("system/bootstrapping"), container); service.Run(); AccountViewPageModel viewModel = new AccountViewPageModel(null, container); viewModel.AccountName = "TestAccount"; viewModel.Email = "email@test.com"; viewModel.Keyword = "test"; viewModel.Password = "my password"; viewModel.Name = "test name"; //act viewModel.OnSaveCommand(); using (IDataContextService dataContextService = container.Resolve<IDataContextService>()) { //Assert var record = dataContextService.Records.Get(r => r.Name == "test name"); Assert.AreEqual("Account", record.Template.Name); Assert.AreEqual("test name", record.Name); Assert.AreEqual("email@test.com", record.Properties.Single(p => p.Name == "Email").Value); Assert.AreEqual("my password", record.Properties.Single(p => p.Name == "Password").Value); Assert.AreEqual("test", record.Keywords.Single(p => p.Name == "test").Name); dataContextService.Delete(); } } } }
结论
目前,以下应用程序正在使用 PhoneCore Framework- SecureBox - 一个 Windows Phone 7 应用程序,允许将敏感信息存储在安全存储中,此处有描述
- Phone Guitar Tab - Windows Phone 7 的吉他谱查看器。提供搜索和下载谱/图片引擎(使用 ultimate-guitar.com 和 last.fm)
关注点
- 性能测量
- 配置改进
- 附加功能实现
更新
该框架可作为 NuGet 包 在此处 获取。
变更
版本 0.6.1 发布于 2012/11/02
- AOP 支持
- 通过特殊工具在编译时生成代理,使用配置信息处理实现某些接口的类型
- 这些类型的自定义方法拦截器
- 容器的更改和改进
- RegisterInstance 方法,注册现有对象实例
- 对象生命周期管理器
- 按名称 RegisterXXX/Resolve
- RegisterXXX 方法的流畅界面
- 重构
- 跟踪:现在跟踪作为注册在容器中的服务使用
- 页面映射:提取接口
版本 0.5.1 发布于 2012/01/20
- 修复了配置路径错误
- 从 ConfigSection 中提取了接口
- 容器支持在配置中定义的默认类型映射注册