Introduction
There is sometimes the need to have a functionality on a class
that is not there, and it is not necessarily a good idea to modify the class
. To accomplish this a behavior is used to take the DataContext
of the bound FrameworkElement
and use this DataContext
to create a new DataContext
. Then all bindings within the FrameworkElement
will then be bound to this new DataContext
.
Background
While working a WPF Drop Down Menu Button control for a project I was working, I ran into issues because the Control
was actually bound to a Model
and did not use a ViewModel
(the control is presenting in the codeproject article WPF Drop Down Menu Button
). The reason is that the ICommand
properties were in the DataContext
for the UserControl
and RelativeBinding
was used to get to those properties. However, the ContextMenu
that is used to contain the DropDownButton
is not in the VisualTree
, so cannot use the RelativeBinding
feature to find ancestors. That meant I had to either encapsulate all the Model
classes into a ViewModel
, or come up with something different. I really did not want to have to change the design that dreastically if I did not have to. I initially tried to initialize the ViewModel
containing the attached properties in XAML for the DataContext
for the control, but that did not work. It thus seemed like it was best to create a behavior to initialize this attached properties ViewModel
using the initial ViewModel
in the DataContext
, and the set the DataContext
to this new ViewModel
.
The Implementation
The behavior that is used to implement this functionality is:
public class AttachedPropertiesBehavior
{
public Type Type { get; set; }
#region static part
public static readonly DependencyProperty ViewModelTypeProperty
= DependencyProperty.RegisterAttached("ViewModelType", typeof(Type),
typeof(AttachedPropertiesBehavior),
new PropertyMetadata(null, delegate(DependencyObject o,
DependencyPropertyChangedEventArgs args)
{
new AttachedPropertiesBehavior(o, (Type) args.NewValue);
}));
public static Type GetViewModelType(FrameworkElement control)
{
return (Type)control.GetValue(ViewModelTypeProperty);
}
public static void SetViewModelType(FrameworkElement control, Type value)
{
control.SetValue(ViewModelTypeProperty, value);
}
#endregion static part
#region instance part
private bool _isBusy;
private readonly FrameworkElement _frameworkElement;
public AttachedPropertiesBehavior(object sender, Type type)
{
_frameworkElement = (FrameworkElement)sender;
CreateNewDataContext(type);
_frameworkElement.DataContextChanged += (s, args)
=> CreateNewDataContext((Type)args.NewValue);
}
private void CreateNewDataContext(Type type)
{
if (_isBusy) return;
_isBusy = true;
Debug.Assert(_frameworkElement.DataContext != null,
$"The was no DataContext for FrameworkElement");
var newDataContext = Activator.CreateInstance(type);
Debug.Assert(newDataContext != null, $"Could not create an instance of type {type}");
var property = type.GetProperty("ViewModel");
Debug.Assert(newDataContext != null,
$"Could not access a 'ViewModel' property for type {type}");
property.SetValue(newDataContext, _frameworkElement.DataContext);
_frameworkElement.DataContext = newDataContext;
_isBusy = false;
}
#endregion instance part
}
This class
has only a single DependencyProperty
that is used to provide the Type
to use to create the new ViewModel
. When this DependencyProperty
is changed, and instance of this Type
is created, and the required property of this Type
, This new ViewModel
is set to the current DataContext
of the FrameworkElement
that this behavior is attached to. If this property does not exist, then the behavior will throw and error. Once the ViewModel
property is set to the DataContext
of the FrameworkElement
, the DataContext
of the FrameworkElement
is set to this new ViewModel
.
This class
is divided into two parts, a static
and an instance. An instance is created the static
part whenever the Type
is changed, and this would probably only occur once. The instance creates the new ViewModel
, and will create a new ViewModel
whenever the DataContact
of the attached FrameworkElement
is changed. It was the need to update the ViewModel
on DataContext
changed, but not when updating the ViewModel
that drove the need for the instance code.
There are two required features of the new attached property ViewModel
class:
- The
class
must have a property with a setter named ViewModel
that will take a class of the Type
of the DataContext
- The
class
must have a default constructor
To make it easier to create the attached property ViewModel
, the sample project has a generic abstract
class
that the property with the name 'ViewModel
' that can be used to create the attached property ViewModel
through inheritance:
public abstract class AttachedPropertiesViewModel<T>
{
public T ViewModel { set; protected get; }
}
The generic T would be the typeof
the original ViewModel
.
An example of inheriting from this class is the following:
public class InsertCommandViewModel : AttachedPropertiesViewModel<ViewModel>
{
public RelayCommand IncrementCommand => new RelayCommand(() => Increment());
private void Increment()
{
ViewModel.Counter++;
}
}
Using the code
The following is an example of using this behavior:.
<Button Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
local:AttachedPropertiesBehavior.ViewModelType="{x:Type local:InsertCommandViewModel}"
Command="{Binding IncrementCommand}"
Content="Increment" />
The ViewModelType
DependencyProperty
is passed the Type
of the ViewModel
to create using the x:Type
tag. It can also be seen that the ViewModel
property for IncrementCommand
is used for the Command
attribute. It is not neccessary to have this behaviour on the FrameworkElement
using the attached properties, it can be used by any child of a container with this behaviour.
Option
There is an article posted by Stein Borge that could be used with this idea: Dynamic WPF MVVM View Model Event, Command and Property Generation. This would allow access to all of the properties of the Model without having to provide code to do it.
History
- 2016/10/07: Initial version