- Introduction
- Overview
- Configuration
- Diagnostic
- Dependency injection container
- Aspect-oriented programming support
- Bootstrapping
- ApplicationBootstrapper class
- BootsrapperService
- Core plugin
- Custom plugins
- Navigation
- Interfaces
- Implementation
- Miscellaneous
- The example of usage: SecureBox application
- Setting up framework
- Business layer
- Data layer
- Database schema
- Initialization
- Presentation layer
- Unit testing
- Conclusion
- Points of interest
PhoneCore Framework is a framework which helps to build applications on Windows Phone 7. It provides:
- Navigation engine which simplifies the navigation between pages
- Dependency injection container which helps to create loosely-coupled applications
- Aspect-oriented programming support by custom method interceptors using custom proxy class generated at compile time (from version 0.6.1)
- Configuration subsystem which allows to manage workflow without rebuilding of an application
- Tracing engine which allows to do postmortem analysis or analyze workflow/performance
- Bootstrapping engine with plugin architecture
- Primitive types: Lazy, Tuple
UPDATE: Since the 0.6.1 release, the framework provides the subset of Aspect-oriented programming features by intercepting of method calls. AOP engine includes:
- proxy generation at compile time by special tool using configuration information for types which implement some interface
- custom interceptors for methods of these types
Container changes and improvements:
- RegisterInstance method that registers existing object instance
- Object lifetime managers
- RegisterXXX/Resolve by name
- Fluent interface for RegisterXXX methods
These changes aren't described here in details. Please visit official project site for the latest information:
Configuration is the heart of the framework code and provides the way to:
- control of application executing flow
- create loosely-coupled classes
- support plugin architecture
- simplify unit testing
Let`s have a look at:
="1.0" ="utf-8"
<settings>
<system>
<container type="PhoneCore.Framework.Container,PhoneCore.Framework">
-->
</container>
-->
<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>
-->
<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>
-->
<storage connectionString="Data Source=isostore:/secureboxdb.sdf">
<init>
-->
<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>
-->
<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>
As you can see, the configuration is stored as xml file in which there are defined the settings of different subsystems such as:
- system/container – configures the settings of dependency injection container, attribute name specify the type of using container
-
system/bootstrapping - defines the tasks for bootstrapper service which should be run only once, e.g. :
- register types in container
- initialize page mapping
- system/diagnostic/tracing - defines the tracing subsystem
- views/pageMapping – defines mapping between pages and its view models
User can define custom settings and easily access them into the code using
ConfigSettings members described in details below.
At the moment, the configuration subsystem consists of the following classes:
-
ConfigSettings – entry point to the configuration. It is a singleton instance:
namespace PhoneCore.Framework.Configuration
{
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
public ConfigSection GetSection(string xpath)
{
return (new ConfigSection(_root)).GetSection(xpath);
}
public IEnumerable<ConfigSection> GetSections(string xpath)
{
return (new ConfigSection(_root)).GetSections(xpath);
}
}
}
As seen from above, it tries to use application.config file as storage
-
ConfigSection – wraps xml node/attribute
namespace PhoneCore.Framework.Configuration
{
public class ConfigSection
{
private ConfigElement _element;
public ConfigSection(ConfigElement element)
{
_element = element;
}
public IEnumerable<ConfigSection> GetSections(string xpath)
{
return _element.GetElements(xpath).Select(e => new ConfigSection(e));
}
public ConfigSection GetSection(string xpath)
{
return new ConfigSection(new ConfigElement(_element.Node, xpath));
}
public string GetString(string xpath)
{
return new ConfigElement(_element.Node, xpath).GetString();
}
public int GetInt(string xpath)
{
ConfigElement element = new ConfigElement(_element.Node, xpath);
return new ConfigElement(_element.Node, xpath).GetInt();
}
public Type GetType(string xpath)
{
return (new ConfigElement(_element.Node, xpath)).GetType();
}
public T GetInstance<T>(string xpath)
{
return (T)Activator.CreateInstance(GetType(xpath));
}
public T GetInstance<T>(string xpath, params object[] args)
{
return (T)Activator.CreateInstance(GetType(xpath), args);
}
}
}
The exposed members are mentioned below:
- GetSection/GetSections – returns one/multiply ConfigSection GetString – returns value as string using
- GetInt – returns value as integer
- GetType – returns value as System.Type
- GetInstance – returns value as instance of type T
These members receive the xpath value as parameter. Unfortunately, this isn’t string which represents any XPath expression. The reason of that is WP7 doesn’t support XPath (Jan 2012). Actually, it is a simple XPath expression with limitations.
-
ConfigElement – encapsulates xml processing operations
namespace PhoneCore.Framework.Configuration
{
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;
}
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));
}
public string GetString()
{
if (IsAttribute) return _attribute.Value;
if (IsNode) return _node.Value;
return null;
}
public int GetInt()
{
return int.Parse(GetString());
}
public new Type GetType()
{
string typeName = GetString();
return Type.GetType(typeName);
}
public XElement Node
{
get { return _node; }
}
public XAttribute Attribute
{
get { return _attribute; }
}
public bool IsAttribute
{
get { return _attribute != null; }
}
public bool IsNode
{
get { return _node != null; }
}
}
}
Here is the example which shows how to use the configuration:
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);
}
Default framework’s diagnostic subsystem allows to store all the major application events into a database and do postmortem analysis or review application workflow, performance, etc. At the moment, trace subsystem is implemented and the following are the most important interfaces:
-
ITrace – represents a tracer for tracing subsystem
namespace PhoneCore.Framework.Diagnostic.Tracing
{
public interface ITrace: IDisposable
{
int Level { get; set; }
...
void Info(ITraceRecord record);
...
void Warn(ITraceRecord record);
...
void Error(ITraceRecord record);
...
void Fatal(ITraceRecord record);
object GetUnderlyingStorage();
ITraceController GetController();
bool IsInitialized { get; }
}
}
As you can see, the interface defines the behavior of logging trace records of different types:
You can interpret these types in your program as you wish
-
ITraceCategory – represents a trace category which helps to group trace records
namespace PhoneCore.Framework.Diagnostic.Tracing
{
public interface ITraceCategory
{
string Name { get; }
}
}
-
ITraceRecord – represents a single trace record
namespace PhoneCore.Framework.Diagnostic.Tracing
{
public interface ITraceRecord
{
ITraceCategory Category { get; }
string Message { get; }
DateTime Date { get; }
IPage Page { get; }
Exception Exception { get;}
Type SourceType { get; }
}
}
You can use the default implementation of tracing which logs trace messages into local database and/or add your custom one. The following section in your
application.config file should be defined:
<diagnostic>
<!---->
<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 framework will create the list of traces define here as shown below:
namespace PhoneCore.Framework.Diagnostic.Tracing
{
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
{
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)
{
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();
}
}
public static ITrace GetTrace()
{
Ensure();
return GetTrace(Default);
}
public static ITrace GetTrace(string name)
{
Ensure();
return __traces[name];
}
}
}
After the definition of the traces in configuration, you can use it using the
TraceFactory class which exposes two static methods to access the configured traces:
- GetTrace() – returns default trace
- GetTrace(string name) – returns trace by name
Here is the example of trace usage:
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));
This code uses the default trace which writes Info message using
"Navigation.To" category which will create automatically at the first call of
TraceCategory.GetInstance():
namespace PhoneCore.Framework.Diagnostic.Tracing
{
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;
}
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];
}
public static IEnumerable<string> GetRegistredCategoryNames
{
get
{
lock (__lockInstance)
return __categories.Keys;
}
}
}
}
Here is an example how the trace output looks:
All major frameworks' components have constructor injection with IContainer instance which represents a Dependency Injection container. It provides the following groups of methods:
- RegisterType - registers mapping between interface and type which implements this interface or registers concrete implementation only
- RegisterInstance - registers an existing instance which implements some interface
- Register - special method which allows to define the interception features programmatically
The instance of container is passed through framework classes and used for registering/resolving of other components, e.g.:
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>();
}
}
This approach has advantages and disadvantages. For example, it allows simplifying creation of unit test (see example below) by avoiding the usage of static instances and /or dependencies at concrete implementations of classes and simplifies the ability to extend/replace frameworks' subsystems. However it hides what the service actually consumes.
Aspect-oriented programming support
Since the version 0.6.1, the framework supports the ability to define custom interceptors which can be attached to methods. This is possible if special proxy class is created for intercepted service at compile time (special tool is already provided for this):
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>();
}
}
}
As you can see, it has stub implementation of interface methods and is inherited from ProxyBase class. The implementation runs the chain of behaviors using the special object of MethodInvocation type. This object contains all necessary information of the executing method of real object. Custom behavior can validate, log, profile, disable and etc. execution of method.
There are two ways how you can attach behaviors to class for which proxy is already generated:
<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>
There are the default execute behavior and custom ones for each component.
container.Register(Component.For<IClassC>().ImplementedBy<ClassC>()
.Proxy<ClassCProxy>()
.AddBehavior(new ExecuteBehavior())
.Singleton());
There is the example of batch file which show how you can auto generate proxy classes using special tool provided via NuGet package or project source codes:
@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
The tool receives the following parameters here:
- Target project root folder where application.config is located
- Assemblies location
- Output directory
Important: the tool is looking for
name attribute of
component node. It interprets the value of the attribute as class name and output file name. If it isn't defined, proxy won't be generated. You can once generate the proxy and remove this attribute to prevent further processing of the component node until the appropriate interface is changed.
Bootstrapping engine runs plugins which should be execute only once at the application startup. The following line need to be added in your
App.xaml:
="1.0" ="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>
<core:ApplicationBootstrapper x:Key="Bootstrapper" d:IsDataSource="False" />
..
</Application.Resources>
…
</Application>
ApplicationBootstrapper class performs the next actions:
- create container
- create bootstrapper service
- run bootstrapper service
BootsrapperService runs the list of defined plugins:
namespace PhoneCore.Framework.Bootstrap
{
public class BootstrapperService: IBootstrapperService
{
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
public bool Run()
{
_trace.Info(_category, "run bootstrapper");
return _plugins.Aggregate(true, (current, task) => current & task.Run());
}
public bool Update()
{
return _plugins.Aggregate(true, (current, task) => current & task.Update());
}
public IBootstrapperPlugin GetPlugin(string name)
{
return _plugins.Single(task => task.Name == name);
}
public IBootstrapperPlugin GetPlugin(Type t)
{
return _plugins.Single(task => task.GetType() == t);
}
public IBootstrapperPlugin GetPlugin<T>()
{
return GetPlugin(typeof(T));
}
#endregion
}
}
The following methods are defined below:
- Run() – execute all plugins by calling its Run() method
- Update() –calls Update() method of each plugin
- GetPlugin() – gets the registered plugin instance by its type or name
The last group of methods provides the access to the separate instance of registered plugin by type or name:
container.Resolve<IBootstrapperService>().GetPlugin<CoreBootstrapperPlugin>();
Let’s look at the major plugin:
CoreBootstrapperPlugin
Core plugin
CoreBootstrapperPlugin plugin registers core services in container:
namespace PhoneCore.Framework.Bootstrap
{
public class CoreBootstrapperPlugin: BootstrapperPlugin
{
public CoreBootstrapperPlugin(ConfigSection config, IContainer container) : base(config, container) { }
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;
}
}
public override bool Update()
{
Trace.Info(Category, "update");
return false;
}
}
}
The services should be defined in configuration and implement framework’s interfaces:
<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>
Afterwards, user code can consume the registered services. They are registered in the container by the core plugin:
- PageMapping
- NavigationService
- FileSystemService
- SettingsService
- IsolatedStorageSettingsService
If there's a need to switch to the usage of your custom implementation, you should create your own implementation of bootstrapper plugin and define it instead of
Core.
You can create your custom plugins which will be executed after
Core plugin. All you need is to create class inherited from
IBootstrapperPlugin:
namespace PhoneCore.Framework.Bootstrap
{
public interface IBootstrapperPlugin
{
string Name { get; }
bool Run();
bool Update();
}
}
and add it in configuration section:
<system>
<!---->
<bootstrapping>
<bootstrappers>
<bootstrapper name="Core" type="PhoneCore.Framework.Bootstrap.CoreBootstrapperPlugin, PhoneCore.Framework" />
<bootstrapper name="Init" type="SecureBox.UI.Infrastructure.Plugins.InitPlugin, SecureBox.UI" />
…
Note: The order is important. The core plugin must be first as the plugins are executed as they are defined.
Navigation engine simplifies the way how your application navigates between pages. It provides the following interfaces:
-
IPage – represents a type of page which is used by mapping for navigation
namespace PhoneCore.Framework.Navigation
{
public interface IPage: IEquatable<IPage>
{
string Name { get; }
}
}
-
INavigationService – defines the behavior of navigation engine
namespace PhoneCore.Framework.Navigation
{
public interface INavigationService
{
event NavigatingCancelEventHandler Navigating;
void NavigateTo(Uri pageUri);
void NavigateTo(Uri pageUri, Dictionary<string, object> parameters);
void NavigateTo(IPage type);
void NavigateTo(IPage type, Dictionary<string, object> parameters);
void GoBack();
}
}
-
IPageResolver – the class which implements this interface should return the corresponding page by its type
namespace PhoneCore.Framework.Navigation
{
public interface IPageResolver
{
IPage Resolve(string name);
}
}
Let’s look how the default implementation works.
The essential part of navigation is page mapping which is defined through configuration:
<views>
<!---->
<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>
As it`s seen, each page is defined by page node:
- name – page name which is unique. The resolver should return the page instance by this
- uri –defines path to this page
- viewModel – viewModel type of the page. It should implement PhoneCore.Framework.Views.IViewModel interface
Also
pageMapping node contains the definition of
IPageResolver type which is used for resolving pages. Now let’s focus on how mapping is implemented:
namespace PhoneCore.Framework.Navigation
{
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);
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;
}
public Uri GetUri(IPage page)
{
return _pageMapping.Single(m => m.Key.Equals(page)).Value.Item1;
}
public IViewModel GetViewModel(IPage page)
{
return _pageMapping.Single(m => m.Key.Equals(page)).Value.Item2.Value;
}
public IPage GetPage(Uri pageUri)
{
return _pageMapping.Keys.Single(m => _pageMapping[m].Item1 == pageUri);
}
}
}
So the constructor creates the dictionary
Dictionary <IPage, Tuple<Uri, Lazy<IViewModel>>> where the key is page type and value is tuple containing “lazy” wrapper for
IViewModel. It prevents from the creation of each ViewModels during
PageMapping initialization.
There are the classes which I haven’t described yet. In brief:
-
Primitive types
- Lazy – provides the implementation of “lazy” type. It is already used by PageMapping class
- Tuple – tuple class
-
PhoneCore.Framework.Views.Views namespace
- IViewMode– defines the behavior of ViewModel
- ViewPage – base class which is used instead of built-in
Let's examine the example of how the application can be implemented with the usage of PhoneCore Framework.
SecureBox is a Windows Phone 7.5 application which allows to store your sensitive information such as account credentials, phone numbers, passwords and prevents the access to it . Currently the development hasn`t done yet, but its code can help to understand the major advantages of using PhoneCore Framework.
Let’s concentrate on the key concepts.
First define
application.config:
="1.0" ="utf-8"
<settings>
<system>
<container type="PhoneCore.Framework.Container,PhoneCore.Framework">
-->
</container>
-->
<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>
-->
<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>
-->
<storage connectionString="Data Source=isostore:/secureboxdb.sdf">
<init>
-->
<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>
-->
<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>
Then change standard MVVMLight's
ViewLocator class to:
public class ViewModelLocator
{
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
[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)); }
}
…
}
Afterwards define
ApplicationBootstrapper as application resource in App.xaml:
="1.0" ="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>
<core:ApplicationBootstrapper x:Key="Bootstrapper" d:IsDataSource="False" />
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
</Application.Resources>
<Application.ApplicationLifetimeObjects>
-->
<shell:PhoneApplicationService Launching="Application_Launching" Closing="Application_Closing" Activated="Application_Activated" Deactivated="Application_Deactivated" />
</Application.ApplicationLifetimeObjects>
</Application>
After these changes the application bootstrapper will be executed automatically. Let’s focus on SecureBox design and code.
Domain entities are represented by simple classes which contain only public properties. These classes are known as Plain Old CLR Objects (POCOs):
There are:
- Record – stores user defined data in single entity
-
Template – represents a generic template for user record. There are the following built-in ones:
- Password
- Account
- Email
- Note
Each template consists of built-in fields which are stored in Properties collection
- Keyword – represents a category of Record. It allows to bind the record to different categories. This helps to search it in the collection
- Property – represents a single field of record which stores the unit of user data
There are the following actions on an entity permitted:
- Creation
- Editing
- Removing (not implemented yet)
There is the following schema of a database which is used for storing our business entities:
The different enterprise patterns can be used to access data stored in database:
- Repository
- Unit of Work
- Transaction script
The entities which are mapped to the tables directly, can be generated by using the auto generation tool:
sqlmetal c:\temp\ secureboxdb.sdf /code:"c:\temp\ secureboxdb.cs" /language:csharp /namespace: SecureBox.UI.Infrastructure.Data /context: SecureBoxDataContext /pluralize
Here is the example of result:
[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
{
Also the command will generate the class derived from
System.Data.Linq.DataContext, which is example of
“Unit of Work” pattern:
[System.Data.Linq.Mapping.DatabaseAttribute(Name="secureboxdb")]
public partial class SecureBoxDataContext : System.Data.Linq.DataContext
{
…
For domain logic, I decided to use
Repository pattern and encapsulate the work with data into single class which is stored in container as
IDataContextService:
namespace SecureBox.UI.Infrastructure.Data
{
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();
}
}
As you can see, it exposes entities related to our domain layer, not to data layer
The right way to initialize and register the code which is working with data is custom bootstrapper plugin:
namespace SecureBox.UI.Infrastructure.Plugins
{
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");
string connectionString =
ConfigSettings.Instance.GetSection("data/storage").GetString("@connectionString");
SecureBoxDataContext dataContext = new SecureBoxDataContext(connectionString);
Container.Register<IDataContextService, DataContextService>(dataContext);
if(dataContext.DatabaseExists())
dataContext.DeleteDatabase();
if (!dataContext.DatabaseExists())
{
CreateDatabase(dataContext);
}
return true;
}
catch (Exception ex)
{
Trace.Fatal(Category, "run is failed", ex);
throw;
}
}
Here is the code, which runs always one time:
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();
}
User interface is constructed by separation of markup and code using MVVM pattern. I use
MVVMLight framework for this:
namespace SecureBox.UI.ViewModel
{
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()
{
}
public virtual void SaveStateTo(IDictionary<string, object> state)
{
}
public virtual void LoadStateFrom(IDictionary<string, object> state)
{
}
}
}
Here is the default view model class which implements PhoneCore interface and derived from MVVMLight’s base class. This approach allows us to use such useful method as
RaisePropertyChanged.
Let’s look at single template:
Account. Here is the markup:
<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">
<!---->
<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"/>
<!---->
<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"/>
<!---->
<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"/>
<!---->
<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>
<!---->
<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"/>
<!---->
<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>
As you may notice, here I’m using
Silverlight toolkit. You should take a look at:
- root tag - I switched to usage of PhoneCore framework page
- data context is set to usage of our ViewModel locator class
Below is the view model of the page:
namespace SecureBox.UI.ViewModel
{
public class AccountViewPageModel: TemplateViewPage
{
public AccountViewPageModel(ConfigSection config, IContainer container)
: base(config, container)
{
PasswordActionIconTappedCommand = new RelayCommand(OnGeneratePassword);
}
#region Methods
private void OnGeneratePassword()
{
Password = Container.Resolve<IPasswordGenerator>().Generate();
RaisePropertyChanged("Password");
}
#endregion
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
}
}
On my opinion the implementation is trivial: it is derived of base class of all templates which inherits ViewModelBase I mentioned before:
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);
}
Let’s look closer at unit test for the view model.
PhoneCore Framework tries to avoid the usage of static classes or method as they interfere to create well tested code. Instead, the dependency injection container is passed through all components of the framework.
Dependency injection and MVVM patterns help to create testable code for your
ViewModel classes . Here is the test which checks the fact whether we can create the record. It emulates the workflow of UI controls of account template:
namespace SecureBox.UI.UnitTests.ViewModel
{
[TestClass]
public class AccountViewPageModelTests
{
[TestMethod]
public void CanSave()
{
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";
viewModel.OnSaveCommand();
using (IDataContextService dataContextService = container.Resolve<IDataContextService>())
{
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();
}
}
}
}
At the moment, the following applications are using the PhoneCore Framework:
- SecureBox - a Windows Phone 7 application which allows to store sensitive information in secure storage, it is described here
- Phone Guitar Tab - guitar tab viewer for Windows Phone 7. Provides search&download tab/images engine (uses ultimate-guitar.com and last.fm)
I will update wiki documentation at
official project site according to the development progress.
- Performance measurement
- Configuration improvement
- Additional feature implementation
Updates
The framework is available as NuGet package here.
Changes:
Version 0.6.1 released 02/11/2012
- AOP support
- proxy generation at compile time by special tool using configuration information for types which implement some interface
- custom interceptors for methods of these types
- Container changes and improvements
- RegisterInstance method that registers existing object instance
- Object lifetime managers
- RegisterXXX/Resolve by name
- Fluent interface for RegisterXXX methods
- Refactoring
- Tracing: now trace is used as service registered in container
- Page mapping: interface is extracted
Version 0.5.1 released 01/20/2012:
- fixed configuration path errors
- extracted interface from ConfigSection
- container supports default type mapping registration which is defined in configuration