A micro-library (not a framework!) that accelerates UI development with high-quality base components that leverage WPF's internals and stay out of your way.
From the Nuget package manager console:
Install-Package WpfIgniter
BindableBase
provides a simple interface on which to build view models. It implements INotifyPropertyChanged
and INotifyPropertyChanging
for maximum flexibility. The primary interaction with BindableBase
is through its SetProperty
protected method. SetProperty
can be used to create bindable properties:
public class MyViewModel : BindableBase {
private string _myProperty;
public string MyProperty {
get { return _myProperty; }
set { SetProperty(ref _myProperty, value); }
}
// etc
}
SetProperty
automatically filters out cases where a property is set but its value is not changed. It also optionally accepts onChanged
and onChanging
handlers to help keep you property setters clean. Finally, you can provide a coercion handler to change incoming values before they're set.
Most modern WPF applications make use of Prism's DelegateCommand
or a similar class that wraps an Action
or a Action<object>
in an ICommand
interface. One of the major drawbacks of these classes is that they rely on CommandManager.RequerySuggested
, which admits:
The CommandManager only pays attention to certain conditions in determining when the command target has changed, such as change in keyboard focus. In situations where the CommandManager does not sufficiently determine a change in conditions that cause a command to not be able to execute, InvalidateRequerySuggested can be called to force the CommandManager to raise the RequerySuggested event.
So, if your command's OnCanExecute
is dependent on, say, changes in your view model, your command will remain enabled or disabled until a UI event occurs or you call a non-UI static method in your view model, breaking the MVVM pattern.
Igniter solves this problem in two ways, depending on your needs.
An ExpressionCommand
is set up like any other command.
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
CommandParameter
is convertible to anint
(the string "123" is convertible, for instance) - the value of the converted
CommandParameter
is greater than 0 - the value of
Value
is greater than 0
Additionally, any time the CommandParameter
changes or MyViewModel
emits a PropertyChanged
event for Value
, the CanExecute
expression will be evaluated.
Even more usefully, CanExecute
expressions may be composed of any valid C# expression and will automatically update if it finds:
INotifyPropertyChanged
- notified properties
DependencyObject
- dependency properties
INotifyCollectionChanged
andIBindingList
- indexer
- methods
This essentially accomplishes the thing you probably expected Prism's DelegateCommand
to do in the first place.
Like its Prism spiritual ancestor, Igniter.DelegateCommand
takes an Action
or an Action<T>
for commands that accept a T
command parameter. It also optionally takes a bool canExecute
parameter at construction to determine if it's available to execute initially. Later on down the line, this can be mutated by calling SetCanExecute
and thus triggering watchers.
The DelegateCommand
has two use cases that differentiate it from ExpressionCommand
:
- A
DelegateCommand
may always be allowed to execute, so it is simpler to setup than anExpressionCommand
, which requires an expression describing when the command may execute. - If the lifetime of the view model is short or if the view model is one of many instances, an
ExpressionCommand
may be too heavy for use due to its expression analysis. In this case, you may simply need to take matters into your own hands.
For example,
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; } }
}
Subscribing to changes in WPF is strangely very error prone. If you subscribe to a INotifyPropertyChanged.PropertyChanged
event, you can only tell which property changed by inspecting the PropertyName
string - a big problem if you refactor your code. Alternatively, if you subscribe to a DependencyObject
's DependencyProperty
using PropertyDescriptor.AddValueChanged
, you can create a memory leak!
Igniter solves both of these problems with a couple extension methods:
INotifyPropertyChanged myViewModel = // ...
IDisposable subscription = myViewModel
.SubscribeToPropertyChanged(vm => vm.MyProperty, OnMyPropertyChanged);
// later, to unsubscribe:
subscription.Dispose();
MyDependencyObject myDepObj = // ...
IDisposable subscription = myDepObj.SubscribeToDependencyPropertyChanges(
myDepObj, MyDependencyObject.MyDependencyProperty,
OnMyDependencyPropertyChanged);
// later, to unsubscribe:
subscription.Dispose();
One of the most important aspects of a modern WPF app is its ability to compose views out of smaller views. In Prism, this is accomplished with magic strings and a great amount of complexity in the IRegionManager
.
Even the most complex of application can be broken down much more simply and more (type-)safely.
Predominately, developers simply want to stick a view/view model pair in a control in a parent view. Igniter accomplishes this with the simplicity you'd expect:
<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>
By defailt, ViewElement
will activate (i.e., instantiate via Activator.CreateInstance
) the given view type and resolve the given view model type via your IoC container through the IViewFactoryResolver
shim provided to the attached ViewFactory
's constructor.
In many casses, your logical tree may reflect your view model object structure and so often times it is prudent to continue to adjust your data context to follow suit. Additionally, when using controls like ItemsControl
your data context necessarily changes to track an item in the list. In both of these cases, it's often useful to be able to get back to your root view model to access a command or a property. To accomplish this, developers typically use {RelativeSource FindAncestor}
to refer to that view model. Aside from the bulkiness associated with this syntax, using a relative source can cause a significant performance penalty when used too often.
To work around these problems, Igniter has the RootViewModelBindingExtension
that will be automatically sourced to the view model bound to the view:
<StackPanel>
<TextBlock Text="{Binding MyProperty}"/>
<Border DataContext="{x:Null}">
<TextBlock Text="{ign:RootViewModelBinding MyProperty}"/>
</Border>
</StackPanel>
An unseen peril of WPF is that if you source a ResourceDictionary
in more than one place, all of its definitions are instantiated once per reference. For more complex apps with many resources this can be a disaster for memory usage.
Using Igniter, you can easily share resources by simply attaching the SharedResourceBehavior
to any FrameworkElement
where you would normally use a <ResourceDictionary Source="..."/>
.
<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>
Some apps with many resources choose to break up those resources into separate files for better organization. The major downside to this approach is that they typically end up with "index.xaml" files that simply list the contents of the folder.
This can be solved more elegantly with DirectoryResourcesBehavior
. Like SharedResourceBehavior
, it may be attached to any FrameworkElement
where you would normally use a <ResourceDictionary Source="..."/>
.
<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>