Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

An active button in WPF

Rate me:
Please Sign up or sign in to vote.
4.72/5 (7 votes)
14 Jul 2014CPOL6 min read 23.2K   9   7   3
Change the appearance of a button that is used in a navigation menu, based on the value of a property of that button.

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"):

Image 1

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:

C#
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);
    }
}   

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:

XML
<Button Content="My CDs" Command="{Binding ChangePageCommand}" CommandParameter="{Binding MyCDsVM}" local:ButtonExtensions.IsActive="True" />

The XML prefix “local” is defined in the user control’s opening tag:

XML
xmlns:local="clr-namespace:ActiveButtonDemo"

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.

C#
public class ActiveButtonExtension : MarkupExtension
    {
        private DataContextFinder _dataContextFinder;
        private string[] _preAndSuffixes;
        private bool _subscribed;

        public ActiveButtonExtension()
        {
            _preAndSuffixes = new string[] { "btn", "ViewModel" };
        }

        /// <summary>
        /// Gets or sets the target Dependency Object from the service provider
        /// </summary>
        protected Button Button { get; set; }

        /// <summary>
        /// Gets or sets the target Dependency Property from the service provider;
        /// </summary>
        protected DependencyProperty IsValidProperty { get; set; }

        /// <summary>
        /// This is the only method that is needed for a MarkupExtension.
        /// All the others are helper methods.
        /// </summary>
        /// <param name="serviceProvider"></param>
        /// <returns></returns>
        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)
                    {
                        //item is a suffix
                        result = name.Substring(0, name.Length - item.Length);
                    }
                    else
                    {
                        //item is a prefix
                        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]);

                    //event handler for currentview changed event (INotifyPropertyChanged)
                    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);
                }

            }
        }

}

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:

XML
<Button Content="My CDs" Name="btnMyCDs" Command="{Binding ChangePageCommand}" 
    CommandParameter="{Binding MyCDsVM}" 
    local:ButtonExtensions.IsActive="{local:ActiveButton}"/>

A Style

It's time for some eye candy. After all, that is what we were after from the beginning.

XML
<Style TargetType="Button" x:Key="ActiveButtonStyle">
    <Setter Property="Background" Value="Yellow"></Setter>
</Style>

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:

C#
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
}

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:

XML
<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>

The XAML for each our Buttons becomes:

XML
<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}}" 
/>

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:

XML
Content="{Binding RelativeSource={RelativeSource Self}, Path=(local:ButtonExtensions.IsActive)}"

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.

C#
if (!_subscribed)
{
    mainVM.PropertyChanged += mainVM_PropertyChanged;
    _subscribed = true;
}

The handler is really simple:

C#
private void mainVM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName.Equals("CurrentPageViewModel"))
    {
        OnDataContextFound();
    }
}

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!

Image 2

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Belgium Belgium
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhy so complicated? Pin
Ian Shlasko14-Jul-14 10:01
Ian Shlasko14-Jul-14 10:01 
AnswerRe: Why so complicated? Pin
bvgheluwe15-Jul-14 9:30
bvgheluwe15-Jul-14 9:30 
QuestionTres bien! Pin
Volynsky Alex13-Jul-14 12:35
professionalVolynsky Alex13-Jul-14 12:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.