Thursday, June 13, 2019

WPF MVVM Attached Behaviors

One thing I have struggled with in MVVM is executing methods on a control without having a reference to the control. For example, the user wants a button they can click on to scroll a large grid to the top. There's no property that can be bound to do this, but the answer is in attached properties.

An attached property is similar to a dependency property, but it is not tightly bound to a particular type of control. It can be attached to any control.

It's not uncommon for these kinds of requirements to be added to mature products. You could create a subclassed control and add a dependency property to handle this requirement, but every datagrid would have to be replaced with the subclassed version which can be very intrusive in a mature product. The attached property approach simply adds new functionality to an existing control.

Start a new WPF C# project and target framework 4.0 or later. Call it Behavior. We will create a list box and a button. When the user clicks the button we will call the SelectAll method of the list box without having a reference to the list box.

Add a new class to hold the behavior and call it SelectAllBehavior. An attached property is declared similarly to a dependency property. I've bolded the important part. It specifies code to be executed when the property value changes. This code receives a reference to the control that the property is bound to. Once we see the property is being set true, we can call the SelectAll method, and set the property back to false.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace Behavior
{
    class SelectAllBehavior
    {
        public static readonly DependencyProperty SelectAllProperty = DependencyProperty.RegisterAttached("SelectAll", typeof(bool),
            typeof(SelectAllBehavior), new PropertyMetadata(false, (o, e) =>
            {
                ListBox lb = o as ListBox;
                if (lb == null) return;
                if ((bool)e.NewValue)
                {
                    lb.SelectAll();
                    // Not sure if there's an easier way to do this, but SetValue doesn't update the bound property
                    BindingExpression be = BindingOperations.GetBindingExpression(lb, SelectAllProperty);
                    string PropertyName = be.ParentBinding.Path.Path;
                    be.DataItem.GetType().GetProperty(PropertyName).SetValue(be.DataItem, false);
                 }
            }
        ));

        public static bool GetSelectAll(DependencyObject o)
        {
            return (bool)o.GetValue(SelectAllProperty);
        }

        public static void SetSelectAll(DependencyObject o, bool value)
        {
            o.SetValue(SelectAllProperty, value);
        }

    }
}


Replace MainWindow.xaml with the following xaml. It defines a command, but more importantly, it attaches the SelectAllBehavior to the list box and binds it to a local property.

<Window x:Class="Behavior.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Behavior"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="SelectCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource SelectCommand}" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <Grid>
    <StackPanel Orientation="Horizontal" Height="100">
        <ListBox local:SelectAllBehavior.SelectAll="{Binding SelectAll, Mode=TwoWay}" SelectionMode="Extended">
            <ListBox.Items>
                <ListBoxItem Content="ALPHA"/>
                <ListBoxItem Content="BETA"/>
                <ListBoxItem Content="GAMMA"/>
            </ListBox.Items>
        </ListBox>
        <Button Height="22" Content="Select All" Command="{StaticResource SelectCommand}"/>
    </StackPanel>
    </Grid>
</Window>


The view model simply defines a handler for the command which sets the SelectAll property true.

using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace Behavior
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private bool _SelectAll;
        public bool SelectAll
        {
            get { return _SelectAll; }
            set
            {
                _SelectAll = value;
                PropChanged("SelectAll");
            }
        }

        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            SelectAll = true;
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void PropChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}

When the user clicks the button the SelectAll property is set true. This executes the code in the attached property which then calls the SelectAll method on the list box.

You could attach this to different list boxes and even controls that are not list boxes, although the code would need to be enhanced to deal with non-listbox controls.

Take a look at the list box before and after the button is clicked.

Before click

After click

No comments:

Post a Comment