All Igniter namespaces are available in the ign: XML namespace (http://schemas.northhorizon.net/igniter) for convenience.
BindableBase provides a simple base class on which to build view models that update views when properties change.
Predominately, implementers will be interested in using the SetProperty protected method which, given a new value and a ref'd current field, applies coercion, calls events as necessary, and sets the backing field. SetProperty makes use of the CallerMemberNameAttribute, so user code does not have to provide the name of the property as a string or lambda.
public class MyViewModel : BindableBase {
private string _myProperty;
public string MyProperty {
get { return _myProperty; }
set { SetProperty(ref _myProperty, value); }
}
// etc
}If a new, coerced value provided to SetProperty has been evaluated to be different than the old value by EqualityComparer<T>.Default, SetProperty will call its provided delegates before calling the common event methods.
public class MyViewModel : BindableBase {
private string _myProperty;
public string MyProperty {
get { return _myProperty; }
set { SetProperty(ref _myProperty, value); }
}
private void OnMyPropertyChanging(string newValue) {
// This will be called first.
}
protected override void OnPropertyChanging(string propertyName) {
// This will be called second.
// calling base will cause the PropertyChanging event to be raised.
base.OnPropertyChanging(propertyName);
}
private void OnMyPropertyChanged() {
// This will be called third.
// NOTE: the value of _myProperty now represents the new value!
}
protected override void OnPropertyChanged(PropertyChangedEventArgs args) {
// This will be called last.
// PropertyChangedEventArgs will actually be a PropertyChangedEventArgs<T>
// calling base will cause the PropertyChanged event to be raised.
base.OnPropertyChanging(propertyName);
}
}Before a new value is evaluated for equality versus an old value, a coercion delegate may be applied.
public class MyViewModel : BindableBase {
private string _myProperty;
public string MyProperty {
get { return _myProperty; }
set { SetProperty(ref _myProperty, value, coerceValue: CoerceMyProperty); }
}
private string CoerceMyProperty(string newValue) {
if (string.IsNullOrEmpty(newValue))
return _myProperty;
if (newValue.Length > 5)
return newValue.Substring(0, 5);
return newValue;
}
}The coercion delegate may return
- the previous value
- the new value
- a different value altogether
In the case where the values do not change (again, according to EqualityComparer<T>.Default), SetProperty will notify the UI directly (but does not raise PropertyChanging or PropertyChanged) to refresh its value so it will be in sync.
Other than SetProperty, BindableBase has a number of convenience methods for raising events and a couple "core" event methods. The source code itself is simple enough to suffice for documentation:
protected void OnPropertyChanged(string propertyName) {
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected void OnPropertyChanged<T>(string propertyName, T oldValue, T newValue) {
OnPropertyChanged(new PropertyChangedEventArgs<T>(propertyName, oldValue, newValue));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) {
PropertyChanged(this, args);
}
protected virtual void OnPropertyChanging(string propertyName) {
PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
}DelegateCommand provides a simple wrapper for an arbitrary delegate into an ICommand interface.
public class MyViewModel {
public MyViewModel() {
FooCommand = new DelegateCommand(DoFoo);
BarCommand = new DelegateCommand<int>(DoBar);
}
public ICommand FooCommand { get; private set; }
public ICommand BarCommand { get; private set; }
private void DoFoo() {
// code here
}
private void DoBar(int cmdParameter) {
// code here
}
}A DelegateCommand<T> provides the additional ability to accept a CommandParameter. If the type of the command parameter implements IConvertible (as all of the primitive types do), DelegateCommand<T> will attempt to convert them to the desired type. This can be useful in circumstances where a command parameter comes from user input or is hard-coded in XAML (and thus a string).
If the provided command parameter is not of the desired type and cannot be converted, the DelegateCommand<T> will evaluate its CanExecute to false.
By default, DelegateCommand allows execution, notwithstanding when the command parameter is incompatible. However, user code may control the executability of DelegateCommand by calling SetCanExecute. Additionally, the initial state of executability may be overridden by providing the optional parameter canExecute to the constructor of the DelegateCommand.
public class MyViewModel {
public MyViewModel() {
FooCommand = new DelegateCommand(
() => _barCommand.SetCanExecute(true));
_barCommand = new DelegateCommand(
() => _barCommand.SetCanExecute(false),
canExecute: false); // canExecute defaults to true
}
public ICommand FooCommand { get; private set; }
private readonly DelegateCommand _barCommand;
public ICommand BarCommand { get { return _barCommand; } }
}DelegateCommands do not accept a delegate for the canExecute argument as it would lead the user to believe the command would update when the lambda changes values. To accomplish this, use an ExpressionCommand instead.
ExpressionCommand allows you to use the familiar DelegateCommand instantiation API, but infers when your canExecute has changed.
public class MyViewModel : BindableBase {
public MyViewModel() {
AddCommand = new ExpressionCommand<int>(DoSomething,
cmdParam => cmdParam > 0 && Value > 0);
}
public ICommand AddCommand { get; private set; }
private void DoSomething(int cmdParameter) {
// ...
}
private int _value;
public int Value {
get { return _value; }
set { SetProperty(ref _value, value); }
}
}In this example, AddCommand will only be enabled if all of the following are true:
- the associated
CommandParameteris convertible to anint(the string "123" is convertible, for instance) - the value of the converted
CommandParameteris greater than 0 - the value of
Valueis greater than 0
Additionally, any time the CommandParameter changes or MyViewModel emits a PropertyChanged event for Value, the CanExecute expression will be evaluated.
CanExecute expressions support monitoring updates from members of multiple source types, based on events:
| Source Type | Member | Triggering Event |
|---|---|---|
INotifyPropertyChanged |
Properties, Fields | PropertyChanged for referenced member |
DependencyObject |
Dependency Properties | Dependency Property Changed† |
INotifyCollectionChanged |
Indexer, Methods ‡ | CollectionChanged |
IBindingList |
Indexer, Methods ‡ | ListChanged for adds, deletes, and resets |
† Dependency property changes are monitored using [`SubscribeToDependencyPropertyChanges`][]
‡ Property changes should be managed by `INotifyPropertyChanged`.
Extensions is a static class that contains extension methods for miscellaneous gaps in WPF.
Extensions.GetService<T> provides a more concise syntax for working with IServiceProvider:
public class MyExtension : MarkupExension {
public override object ProvideValue(IServiceProvider serviceProvider) {
IUriContext uriContext = serviceProvider.GetService<IUriContext>();
// ...
}
}Extensions.ResolvePartUri is a wrapper for PackUriHelper.ResolvePartUri but uses an IUriContext as the base path and an arbitrary URI to resolve, relative to that URI context. If the given URI is absolute, then that URI will be returned as a part URI.
// In File: pack://application:,,,/Igniter.Tests.Live;component/test.xaml
uriContext.ResolvePartUri(new Uri("foo.xaml", UriKind.Relative))
// -> /Igniter.Tests.Live;component/foo.xamlNotifyPropertyChangedExtensions is a static class with extension methods for INotifyProeprtyChanged allowing user code to subscribe to changes in a type-safe, refactorable manner.
INotifyPropertyChanged myViewModel = // ...
IDisposable subscription = myViewModel
.SubscribeToPropertyChanged(vm => vm.MyProperty, OnMyPropertyChanged);
// later, to unsubscribe:
subscription.Dispose();A stream of property changes can easily be obtained using the GetPropertyChanges extension method, returning an IObservable<T>.
INotifyPropertyChanged myViewModel = // ...
IDisposable subscription = myViewModel
.GetPropertyChanges(vm => vm.MyProperty)
.Subscribe(OnMyPropertyChanged);
// later, to unsubscribe:
subscription.Dispose();If the underlying implementation of INotifyPropertyChanged raises PropertyChangedEventArgs<T> (as BindableBase does), instead of compiling the given lambda expression, GetPropertyChanges will simply obtain the new values from the event arguments.
There is also a parallel subscription method for INotifyPropertyChanging.
INotifyPropertyChanging myViewModel = // ...
IDisposable subscription = myViewModel
.SubscribeToPropertyChanges(vm => vm.MyProperty, OnMyPropertyChanging);
// later, to unsubscribe:
subscription.Dispose();DependencyObjectExtensions.SubscribeToDependencyPropertyChanges is an extension method that allows user code to subscribe to the changes of a DependencyProperty without creating a memory leak.
MyDependencyObject myDepObj = // ...
IDisposable subscription = myDepObj.SubscribeToDependencyPropertyChanges(
myDepObj, MyDependencyObject.MyDependencyProperty,
OnMyDependencyPropertyChanged);
// later, to unsubscribe:
subscription.Dispose();This is accomplished by using an attached behavior bound to the desired property to proxy value changes. The proxy maintains a hard reference to subscribing delegates, but, once the target DependencyObject is released from memory, the proxies may also be garbage collected.
As a convenience, there is also a GetDependencyPropertyChanges which returns an IObservable<object> for monitoring changes.
MyDependencyObject myDepObj = // ...
IDisposable subscription = myDepObj
.GetDependencyPropertyChanges(myDepObj, MyDependencyObject.MyDependencyProperty)
.Subscribe(OnMyDependencyPropertyChanged);
// later, to unsubscribe:
subscription.Dispose();ViewFactory can create and bind views and view models based on specified strategies. It implements the IViewFactory interface for dependency injection into view models and relies on the IViewFactoryResolver to shim the IoC container of the library user's choice.
ViewFactory is meant to be part of your dependency injection environment and is agnostic to what library you are using in your application. To use it,
- register an implementation of
IViewFactoryResolverwrapping a resolution object from your dependency injection framework. - register
ViewFactoryas the implementation ofIViewFactoryat whatever level and for whatever scopes necessary.
When using AutoFac, it's generally considered good practice to use modules to contain your registrations. Along these lines, a very simple module could be created to register ViewFactory correctly. A private class is being used to hide the IContainer shim.
public class IgniterModule : Module {
protected override void Load(ContainerBuilder builder) {
base.Load(builder);
builder.RegisterType<ViewFactoryResolver>().As<IViewFactoryResolver>();
builder.RegisterType<ViewFactory>().As<IViewFactory>();
}
private class ViewFactoryResolver : IViewFactoryResolver {
private readonly IContainer _container;
public ViewFactoryResolver(IContainer container) {
_container = container;
}
public object Resolve(Type type) {
return _container.Resolve(type);
}
public T Resolve<T>() {
return _container.Resolve<T>();
}
}
}To use ViewFactory with Unity, simply add it to your registrations. In this example, a private class is used to hide the IUnityContainer shim.
public partial class App : Application {
public App() {
var container = new UnityContainer();
container
.RegisterType<IViewFactoryResolver, ViewFactoryResolver>()
.RegisterType<IViewFactory, ViewFactory>();
}
private class ViewFactoryResolver : IViewFactoryResolver {
private readonly IUnityContainer _container;
public ViewFactoryResolver(IUnityContainer container) {
_container = container;
}
public object Resolve(Type type) {
return _container.Resolve(type);
}
public T Resolve<T>() {
return _container.Resolve<T>();
}
}
}With Castle Windsor, an installer can easily be configured to register ViewFactory. A private class is being used to hide the IWindsorContainer shim.
public class IgniterInstaller : IWindsorInstaller {
public void Install(IWindsorContainer container, IConfigurationStore store) {
container
.Register(Component
.For<IViewFactoryResolver>()
.ImplementedBy<ViewFactoryResolver>())
.Register(Component
.For<IViewFactory>()
.ImplementedBy<ViewFactory>());
}
private class ViewFactoryResolver : IViewFactoryResolver {
private readonly IWindsorContainer _container;
public ViewFactoryResolver(IWindsorContainer container) {
_container = container;
}
public object Resolve(Type type) {
return _container.Resolve(type);
}
public T Resolve<T>() {
return _container.Resolve<T>();
}
}
}Once you have registered IViewFactory correctly, you can take advantage of its methods in a mockable fashion in your view models.
public class MyViewModel {
public MyViewModel(IViewFactory viewFactory) {
MyView childView;
MyViewModel childViewModel;
viewFactory.Create(ref myView, ref myViewModel);
myViewModel.DoSomething();
ChildView = myView;
}
public MyView ChildView { get; private set; }
}To further improve mockability, you may also choose to receive a dynamic for a view, instead of a strongly-typed view. Of course, this also means you must specify the type parameters to Create.
public class MyViewModel {
public MyViewModel(IViewFactory viewFactory) {
dynamic childView;
MyViewModel childViewModel;
viewFactory.Create<MyView, MyViewModel>(ref myView, ref myViewModel);
childView.Background = Brushes.White;
myViewModel.DoSomething();
ChildView = myView;
}
public dynamic ChildView { get; private set; }
}Create employs one of three creation strategies to construct your view and view model:
CreationStrategy |
Underlying Service | Parameter Direction |
|---|---|---|
Activate |
Activator.CreateInstance |
out |
Resolve |
IViewFactoryResolver |
out |
Inject |
Calling code | in |
By default, Create will activate your views and resolves your view models. This can be overridden by supplying one or both of the optional creationStrategy parameters.
var myView = GetSomeView();
MyViewModel myViewModel;
viewFactory.Create(
ref myView, ref myViewModel,
viewCreationStrategy: CreationStrategy.Inject,
viewModelCreationStrategy: CreationStrategy.Activate);ViewElement is a XAML proxy for the ViewFactory allowing users to compose views more easily without relying on a view model.
Before ViewElement can be used, a ViewFactory must be attached to an ancestor in the visual tree. This is done automatically by ViewFactory.Create. If you have a parent tree that does not have a ViewFactory attached, you can attach one manually by calling viewFactory.Attach(frameworkElement). Note that Attach is on ViewFactory itself, not IViewFactory.
<UserControl xmlns:ign="http://schemas.northhorizon.net/igniter">
<StackPanel>
<ign:ViewElement ViewType="local:MyFirstView"
ViewModelType="local:MyFirstViewModel"/>
<ign:ViewElement ViewType="local:MySecondView"
ViewModelType="local:MySecondViewModel"/>
</StackPanel>
</UserControl>As an analog to ViewFactory, ViewElement uses the same creation strategies and defaults as ViewFactory.Create. Similarly, these creation strategies can be overridden with one or both of their respective CreationStrategy attributes:
<ign:ViewElement ViewType="local:MyFirstView"
ViewCreationStrategy="Resolve"
ViewModel="{Binding MyViewModel}"
ViewModelCreationStrategy="Inject"/>When using CreationStrategy.Activate or CreationStrategy.Resolve, provide the ViewType or ViewModelType as appropriate. Conversely, when using CreationStrategy.Inject, provide View or ViewModel as appropriate:
ViewCreationStrategy |
ViewModelCreationStrategy |
Required Attributes |
|---|---|---|
Activate or Resolve |
Activate or Resolve |
ViewType, ViewModelType |
Activate or Resolve |
Inject |
ViewType, ViewModel |
Inject |
cAtivate or Resolve |
View, ViewModelType |
Inject |
Inject |
View, ViewModel |
Finally, ViewElement has an attribute called RecreationOptions to configure whether a view or view model should be recreated when its view model or view definition changes.
<ign:ViewElement ViewType="local:MyFirstView"
ViewModelType="local:MyFirstViewModel"
RecreationOptions="RecreateView, RecreateViewModel"/>RecreationOptions |
Changed Attribute | Components Recreated |
|---|---|---|
None |
View, ViewType |
View |
None |
ViewModel, ViewModelType |
View Model |
RecreateView |
View, ViewType |
View |
RecreateView |
ViewModel, ViewModelType |
View, View Model |
RecreateViewModel |
View, ViewType |
View, View Model |
RecreateViewModel |
ViewModel, ViewModelType |
View Model |
RecreateView, RecreateViewModel |
View, ViewType, ViewModel, ViewModelType |
View, View Model |
RootViewModelBindingExtension is a markup extension allowing user code to access the view model bound to the current view regardless of what the current data context is.
<StackPanel>
<TextBlock Text="{Binding MyProperty}"/>
<Border DataContext="{x:Null}">
<TextBlock Text="{ign:RootViewModelBinding MyProperty}"/>
</Border>
</StackPanel>A RootViewModelBinding supports virtually all of the properties of a normal Binding except for, of course, Source, RelativeSource, and ElementName.
The SharedResourceBehavior attached behavior allows multiple FrameworkElements to share resource dictionaries so that each reference does not re-instantiate that dictionary's resources.
<UserControl xmlns:ign="http://schemas.northhorizon.net/igniter"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<i:Interaction.Behaviors>
<ign:SharedResourceBehavior Source="../path/to/resources.xaml"/>
</i:Interaction.Behaviors>
<Border Background="{StaticResource MyBackgroundResource}"/>
</UserControl>The SharedResourceBehavior retrieves the desired dictionary from the cache and adds it to the Resources of its associated object when it is attached.
The references to dictionaries are weak, so once all referencing views are garbage-collectable, the shared resources will be garbage-collectable as well. Resources that should be available permanently in the application should be added to the App resources.
To refer to all of the resource dictionaries in a given directory and (optionally) its subdirectories, use a DirectoryResourcesBehavior attached behavior. The behavior can be attached to any FrameworkElement and merges in all of the XAML resources found in the folder.
<UserControl xmlns:ign="http://schemas.northhorizon.net/igniter"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<i:Interaction.Behaviors>
<ign:DirectoryResourcesBehavior Directory="../path/to/resources_folder"/>
</i:Interaction.Behaviors>
<Border Background="{StaticResource MyBackgroundResource}"/>
</UserControl>By default, SharedResourcesBehavior includes subdirectories and uses the same cache as SharedResourceBehavior to add resources. These defaults can be overridden with the IsSubdirectoriesIncluded and IsShared attributes, respectively.
<UserControl xmlns:ign="http://schemas.northhorizon.net/igniter"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<i:Interaction.Behaviors>
<ign:DirectoryResourcesBehavior Directory="../path/to/resources_folder"
IsSubdirectoriesIncluded="false"
IsShared="false"/>
</i:Interaction.Behaviors>
<Border Background="{StaticResource MyBackgroundResource}"/>
</UserControl>