Introduction
In this article, I present a way to change the appearance of a button that is used in a navigation menu, based on the value of a property of that button.
The attached source code provides a Visual Studio solution with 2 projects:
- ActiveButtonDemoStart: application that serves as a foundation (see paragraph "Setting the scene")
- ActiveButtonDemo: the finalised application, i.e. with buttons that change appearance
Background
We’ll be using the following concepts:
If you’re not familiar with these, please refer to the documents I linked to.
Using the code
Setting the scene
Our starting point is a simple application (provided in the attached solution as the project "ActiveButtonDemoStart"):
The user can click the buttons in the left pane and the content in the right pane changes accordingly. This is nicely implemented using MVVM.
Suppose you want your users to have a visual clue about where they are in the application. You want the button that corresponds with the content in the right pane to be styled differently. You want the concept of an "active button".
Unfortunately, WPF doesn't provide this out of the box. Fortunately, however, it does provide us with the capabilities to attach custom data to a DependencyObject (like a Button) without having to subclass it: attached properties.
An attached property
Let’s look at the implementation:
public class ButtonExtensions
{
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.RegisterAttached(
"IsActive"
, typeof(bool)
, typeof(ButtonExtensions)
, new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)
);
public static void SetIsActive(DependencyObject element, bool value)
{
element.SetValue(ButtonExtensions.IsActiveProperty, value);
}
public static bool GetIsActive(DependencyObject element)
{
return (bool)element.GetValue(ButtonExtensions.IsActiveProperty);
}
} Colourised in 6ms
I created a class named ButtonExtensions. The name doesn’t matter. I register the property as an attached property. I also provide a setter and a getter method. These will be used by the XAML parser to set the value with regard to the DependencyObject, in our case a Button.
Note that none of this is tied to a Button. We could also use this property on another element.
We use the property in XAML as follows:
<Button Content="My CDs" Command="{Binding ChangePageCommand}" CommandParameter="{Binding MyCDsVM}" local:ButtonExtensions.IsActive="True" />Colourised in 1ms
The XML prefix “local” is defined in the user control’s opening tag:
xmlns:local="clr-namespace:ActiveButtonDemo"Colourised in 0ms
We can now put a button in an active state in XAML, but hard coding it in the markup is not what we want. We need to find a way to decide at runtime whether the value is true or false. Let's use a markup extension.
A markup extension
In the implementation of a markup extension, we can use our own logic that will result in a value for the property (in this case: true or false for IsActive). We will base the decision about the button being active or not on the Name of the button and the typename of the view model that is the CurrentPageViewModel (see MainViewModel).
We must first introduce a convention. The signifcant part of the Name of the button should be equal to the significant part of the typename of the view model. The significant part is the part of the string that remains after removing prefixes and suffixes such as btn, ViewModel, etc.
For example:
Button.Name = "btnMyCDs"
view model typename = “MyCDsViewModel”
=> significant part = "MyCDs"
Let's implement the markup extension.
public class ActiveButtonExtension : MarkupExtension
{
private DataContextFinder _dataContextFinder;
private string[] _preAndSuffixes;
private bool _subscribed;
public ActiveButtonExtension()
{
_preAndSuffixes = new string[] { "btn", "ViewModel" };
}
protected Button Button { get; set; }
protected DependencyProperty IsValidProperty { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
IProvideValueTarget pvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if(pvt != null)
{
IsValidProperty = pvt.TargetProperty as DependencyProperty;
Button = pvt.TargetObject as Button;
_dataContextFinder = new DataContextFinder(Button, OnDataContextFound);
_dataContextFinder.FindDataContext();
if (_dataContextFinder.DataContext == null)
{
_dataContextFinder.SubscribeToChangedEvent();
}
else
{
OnDataContextFound();
}
}
return false;
}
private string GetSignificantPart(string name)
{
string result = name;
int position;
foreach (string item in _preAndSuffixes)
{
position = name.IndexOf(item);
if (position > -1)
{
if (position + item.Length == name.Length)
{
result = name.Substring(0, name.Length - item.Length);
}
else
{
result = name.Substring(position + item.Length);
}
break;
}
}
return result;
}
private void OnDataContextFound(){
if (string.IsNullOrWhiteSpace(Button.Name))
{
return;
}
string name = GetSignificantPart(Button.Name);
string typeName = null;
if (_dataContextFinder.DataContext != null)
{
var mainVM = _dataContextFinder.DataContext as MainViewModel;
if (mainVM != null)
{
string[] nameParts = mainVM.CurrentPageViewModel.GetType().FullName.Split(new string[] { "." }, StringSplitOptions.None);
typeName = GetSignificantPart(nameParts[nameParts.Length - 1]);
if (!_subscribed)
{
mainVM.PropertyChanged += mainVM_PropertyChanged;
_subscribed = true;
}
}
}
if (typeName != null)
{
bool isActive = typeName.Equals(name);
UpdateProperty(isActive);
}
}
private void mainVM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("CurrentPageViewModel"))
{
OnDataContextFound();
}
}
private void UpdateProperty(bool isActive)
{
if (Button != null && IsValidProperty != null)
{
Action update = () => Button
.SetValue(IsValidProperty, isActive);
if (Button.CheckAccess())
{
update();
}
else
{
Button.Dispatcher.Invoke(update);
}
}
}
}Colourised in 104ms
You can see that there’s a lot going on. You might also have noticed the presence of a DataContextFinder. The reason for its existence is the fact that we cannot rely on the DataContext being available at the moment the ProvideValue method is called.
I googled a bit and found these articles that provide a solution, but as you noticed, it’s quite a detour: http://peteohanlon.wordpress.com/2012/11/21/of-mice-and-men-and-computed-observables-oh-my/
http://www.thomaslevesque.com/2009/07/28/wpf-a-markup-extension-that-can-update-its-target/
Thanks to the respective authors!
I gave it my own twist by putting the logic that is concerned with finding the data context in a separate class (DataContextFinder), because it might be useful in other situations as well (not only in markup extensions).
Now we can modify the XAML by using our brand new markup extension as follows:
<Button Content="My CDs" Name="btnMyCDs" Command="{Binding ChangePageCommand}"
CommandParameter="{Binding MyCDsVM}"
local:ButtonExtensions.IsActive="{local:ActiveButton}"/>Colourised in 1ms
A Style
It's time for some eye candy. After all, that is what we were after from the beginning.
<Style TargetType="Button" x:Key="ActiveButtonStyle">
<Setter Property="Background" Value="Yellow"></Setter>
</Style>Colourised in 2ms
This is a very simple style, just to show some difference with a normal button. To make this useful, you would have to provide setters for a lot of other properties.
A Type Converter
Because the value of the IsActive property (on which the decision for the style) is only known at runtime, we must find a way to apply this style dynamically.
At first, I thought of using a style selector (inherited from System.Windows.Controls.StyleSelector), but apparantly we can't tell a Button to use one. Button has no property to set a StyleSelector.
We can, however, assign the style through data binding. The style depends on the IsActive property, so in our binding expression, we should refer to it. Because the types of the properties IsActive (boolean) and Style (System.Windows.Style) do not correspond, we need a type converter (which we also can specify in the binding expression). This is a class that implements the interface IValueConverter:
class ActiveButtonStyleConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Uri resourceLocater = new Uri("/ActiveButtonDemo;component/Styles.xaml", System.UriKind.Relative);
ResourceDictionary resourceDictionary = (ResourceDictionary)Application.LoadComponent(resourceLocater);
bool isActive = bool.Parse(value.ToString());
return isActive ? resourceDictionary["ActiveButtonStyle"] as Style : resourceDictionary["ButtonStyle"] as Style;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}Colourised in 7ms
The style we created should be put in a resource dictionary because we have to be able to find and use this style in the type converter. I called it Styles.xaml and it looks like this:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="Button" x:Key="ActiveButtonStyle">
<Setter Property="Background" Value="Yellow"></Setter>
</Style>
<Style TargetType="Button" x:Key="ButtonStyle">
<Setter Property="Background" Value="Gray"></Setter>
</Style>
</ResourceDictionary>Colourised in 4ms
The XAML for each our Buttons becomes:
<Button Content="My CDs" Name="btnMyCDs" Command="{Binding ChangePageCommand}"
CommandParameter="{Binding MyCDsVM}"
local:ButtonExtensions.IsActive="{local:ActiveButton}"
Style="{Binding Path=(local:ButtonExtensions.IsActive), RelativeSource={RelativeSource Self}, Converter={StaticResource activeButtonStyleConverter}}"
/>Colourised in 3ms
The Style property is assigned a value through data binding. Note that we use RelativeSource in the binding, because the binding needs access to the Button (and not the data context, which is MainViewModel, in this case). We refer to the IsActive property by specifying a Path. By using data binding, the style would be automatically updated when IsActive changes.
As you can see in MainViewModel’s constructor, I already set a value for CurrentView. This means that one view is always active at startup. The corresponding button is styled as specified (with a yellow background). Unfortunately, a markup extension is evaluated only once. If you click another button, the styles are not adjusted appropriately, because the value of the IsActive property doesn’t change. You can verify this at runtime by binding Button’s Content property to the IsActive property:
Content="{Binding RelativeSource={RelativeSource Self}, Path=(local:ButtonExtensions.IsActive)}"Colourised in 0ms
This is one last problem we have to overcome. Fortunately, the MainViewModel class implements INotifyPropertyChanged. In the ActiveButtonExtension, when the DataContext is found, we can subscribe to this event.
if (!_subscribed)
{
mainVM.PropertyChanged += mainVM_PropertyChanged;
_subscribed = true;
}Colourised in 1ms
The handler is really simple:
private void mainVM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("CurrentPageViewModel"))
{
OnDataContextFound();
}
}Colourised in 3ms
When the value of CurrentPageViewModel changes, the logic that sets the value on the IsActive property is executed. When IsActive changes, the style is updated through databinding.
So here we have it: when we click a button, its appearance changes!
Conclusion
What started out as a fairly simple requirement involves quite an amount of code. It's nice that WPF offers all these possibilities, but to be honest, it baffles me how complex it has become.
History
- 2014-07-13: submitted
- 2014-07-14: fixed some typo's and added a screenshot of the final result