Custom Commanding Behavior in WPF TextBox using System.Windows.Interactivity namespace

Posted by: Mahesh Sabnis , on 1/13/2015, in Category WPF
Views: 38653
Abstract: System.Windows.Interactivity helps us to associate custom commanding for those WPF elements who do not have the Command property exposed. This article demonstrates how to implement custom behavior without making use of a 3rd party framework.

Windows Presentation Foundation (WPF) is a widely used technology for creating desktop UI enterprise applications. WPF out-of-the-box provides numerous features for developing Line of Business (LOB) applications. One of them is Commanding which allows a developer to implement no-code behind development. This helps to implement Model-View-ViewModel (MVVM) approach in WPF development. WPF out-of-the-box provides the Command property for elements like Button, MenuItem, RadioButton etc. Advantage of this property is we can define method in the ViewModel and then by defining the Command object which accepts ViewModel method address, we can bind the command object to the command property of the button. This executes the method in the ViewModel when the button is clicked. Implementing WPF Commanding feature is covered over here Creating Application using Windows Presentation Foundation (WPF) Commanding where I have already explained about commanding in WPF for Buttons.

 

In the scenario where we have buttons for executing business operations, e.g. Create, Update, etc, having commands for such operations is acceptable. But what if we have TextBoxes and Lists on UI where we need to implement such commanding behavior? Naturally in this case we need to implement some custom logic where we can associate the custom commanding behavior for such elements. There are frameworks using which we can implement custom commanding, e.g. Prism and MVVM Light Toolkit. Implementing custom commanding using Prism, is explained at WPF 4: Custom Command and Command Parameter for TextBox using Prism 4 and using MVVM Light Toolkit it is explained at Using MVVM Light in WPF for Model-View-ViewModel implementation. Both these approaches require us to use a third party framework which might not get approvals in some companies or might not suit a project. So the question is how to implement custom commanding behavior for elements like TextBox.

In Expression Studio 4.0, we are provided with System.Windows.Interactivity assembly. This assembly is available at C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.5\Libraries\System.Windows.Interactivity.dll

This assembly is available with Visual Studio 2012 and onwards. This assembly provides System.Windows.Interactivity namespace with several classes. To define custom commanding in our application we will make use of the following classes:

· InvokeCommandAction - This executes ICommand when invoked on the specific event of the element.

· EventTrigger - This represents a Trigger which listens for a specific event of its source element and fires when this event is fired.

· Interaction - This is a static class and owns triggers and behavior attached properties. This handles propagations of the change notification of AssociatedObject.

Step 1: Open Visual Studio (the demo of this article is implemented using Visual Studio 2013, ultimate with Update 3) and create a new WPF application targeted to .NET 4.5.1. Name this project as ‘WPF_Interactivity’. In this project add three folders of the name Models, ViewModels and Commands. Add a reference to System.Windows.Interactivity.dll

Step 2: In the Models folder add a new class file and add the following code in it:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace WPF_Interactivity.Models
{
/// <summary>
/// The Product Entity Class.
/// </summary>
public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string Manufacturer { get; set; }
    public string DealerName { get; set; }
    public int Price { get; set; }
}


/// <summary>
/// Product Database class
/// This stores Product Information
/// </summary>
public class ProductDatabase : ObservableCollection<Product>
{
    public ProductDatabase()
    {
        Add(new Product() {ProductId=1,ProductName="Router-16",Manufacturer="MS-Tech",DealerName="MRS",Price=1000});
        Add(new Product() { ProductId = 2, ProductName = "DVDs-10GB", Manufacturer = "Power-Sol", DealerName = "LS", Price = 1200 });
        Add(new Product() { ProductId = 3, ProductName = "Router-32", Manufacturer = "Power-Sol", DealerName = "MRS", Price = 1200 });
        Add(new Product() { ProductId = 4, ProductName = "MotherBoard-DV", Manufacturer = "MS-Tech", DealerName = "LMS", Price = 1050 });
        Add(new Product() { ProductId = 5, ProductName = "DVDs-16GB", Manufacturer = "MR-Sol", DealerName = "TMS", Price = 1050 });
        Add(new Product() { ProductId = 6, ProductName = "Router-8", Manufacturer = "MS-Tech", DealerName = "MRS", Price = 1010 });
        Add(new Product() { ProductId = 7, ProductName = "Monitors-GL", Manufacturer = "Power-Tech", DealerName = "LMS", Price = 1200 });
        Add(new Product() { ProductId = 8, ProductName = "MotherBoard-V7", Manufacturer = "IT-Sol", DealerName = "TMS", Price = 1100 });
        Add(new Product() { ProductId = 9, ProductName = "RAM-DDR2", Manufacturer = "Power-Sol", DealerName = "TMS", Price = 1230 });
        Add(new Product() { ProductId = 10, ProductName = "RAM-DDR4", Manufacturer = "MR-Sol", DealerName = "MRS", Price = 4000 });
        Add(new Product() { ProductId = 11, ProductName = "DVD-RW-60S", Manufacturer = "Power-Tech", DealerName = "MRS", Price = 2200 });
        Add(new Product() { ProductId = 12, ProductName = "Monitor-HD", Manufacturer = "MS-Tech", DealerName = "LMS", Price = 4400 });
        Add(new Product() { ProductId = 13, ProductName = "Graphics-Card-3D", Manufacturer = "MS-Sol", DealerName = "LMS", Price = 4200 });
        Add(new Product() { ProductId = 14, ProductName = "Router-16", Manufacturer = "Power-Sol", DealerName = "TMS", Price = 1040 });
        Add(new Product() { ProductId = 15, ProductName = "RAM-DDR3", Manufacturer = "MR-Sol", DealerName = "TMS", Price = 1020 });
        Add(new Product() { ProductId = 16, ProductName = "Monitor-3D", Manufacturer = "MR-Sol", DealerName = "MRS", Price = 5100 });
        Add(new Product() { ProductId = 17, ProductName = "MotherBoard-3D", Manufacturer = "Power-Tech", DealerName = "LMS", Price = 2200 });
        Add(new Product() { ProductId = 18, ProductName = "Modem-16X", Manufacturer = "MR-Sol", DealerName = "TMS", Price = 5600 });
        Add(new Product() { ProductId = 19, ProductName = "Modem-32X", Manufacturer = "Power-Sol", DealerName = "MRS", Price = 1056 });
        Add(new Product() { ProductId = 20, ProductName = "Router-16", Manufacturer = "MS-Tech", DealerName = "LMS", Price = 1000 });
    }
}



/// <summary>
/// The DataAccess class. This contains GetProducts method which accepts
/// search criteria and filter value to search values based upon the criteria
/// </summary>
public class DataAccess
{
    /// <summary>
    /// Method to filter Data 
    /// </summary>
    /// <param name="criteria">The Property name based on which data will be searched</param>
    /// <param name="filter">The values which will be used for matching the property value</param>
    /// <returns></returns>
    public ObservableCollection<Product> GetProducts(string criteria, string filter)
    {
        ObservableCollection<Product> Products = new ObservableCollection<Product>();
        List<Product> pds = new List<Product>();
        
        if (criteria.Length != 0 && filter.Length != 0)
        {
            switch (criteria)
            {
                case "ProductName":
                    pds = (from p in new ProductDatabase()
                           where p.ProductName.StartsWith(filter)
                           select p).ToList();
                    break;
                case "Manufacturer":
                    pds = (from p in new ProductDatabase()
                           where p.Manufacturer.Contains(filter)
                           select p).ToList();
                    break;
                case "DealerName":
                    pds = (from p in new ProductDatabase()
                           where p.DealerName.Contains(filter)
                           select p).ToList();
                    break;
            }
            foreach (var item in pds)
            {
                Products.Add(item);
            }
        }
        
        return Products;
    }
}
}

The above code contains the following classes:

· Product - this class acts as an entity class containing properties for Product Information

· ProductDatabase - this class acts as a database for the product information

· DataAccess – this class provides mechanism for data access for product data information. This defines ‘GetProducts()’ method with two strings parameters. These parameters are used to filter products based on the criteria and filter value entered by the end-user from UI.

Step 3: In the Commands folder add a new class file with the code for creating command object:

using System;
using System.Windows.Input;

namespace WPF_Interactivity.Commands
{
    public class RelayCommand : ICommand
    {
        Action _handler;
        public RelayCommand(Action h)
        {
            _handler = h;
        }

        public bool CanExecute(object parameter)
        {
            bool action = false;

            if (_handler != null)
            {
                action = true;
            }
            return action;
        }

        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)
        {
            _handler();
        }
    }
}

Step 4: In the ViewModels folder add a class file with the following code:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WPF_Interactivity.Commands;
using WPF_Interactivity.Models;

namespace WPF_Interactivity.ViewModels
{
public class FilterViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    ObservableCollection<Product> _Products;

    public ObservableCollection<Product> Products
    {
        get { return _Products; }
        set { _Products = value; }
    }

    DataAccess objDs;

    
    ObservableCollection<string> _Properties;

    public ObservableCollection<string> Properties
    {
        get { return _Properties; }
        set { _Properties = value; }
    }

    void LoadProperties()
    {
        Properties.Add("ProductName");
        Properties.Add("Manufacturer");
        Properties.Add("DealerName");
    }

    public FilterViewModel()
    {
        Products = new ObservableCollection<Product>();
        Properties = new ObservableCollection<string>();
        objDs = new DataAccess();
        LoadProperties();
        FilterCommand = new RelayCommand(Get); 
    }

    public RelayCommand FilterCommand { get; set; }

    string _Criteria;

    public string Criteria
    {
        get { return _Criteria; }
        set 
        {
            _Criteria = value;
            OnPropertyChanged("Criteria");
        }
    }

    string _Filter;

    public string Filter
    {
        get { return _Filter; }
        set 
        {
            _Filter = value;
            OnPropertyChanged("Filter");
        }
    }



    void Get()
    {
        Products.Clear();
        foreach (var item in objDs.GetProducts(Criteria,Filter))
        {
            Products.Add(item);
        }
    }

    void OnPropertyChanged(string pName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(pName));
        }
    }
}

}

As specified in the comments, this ViewModel class defines the following purpose:

1. Implements INotifyPropertyChanged interface to generate change notification

2. Defines public string type properties of name Criteria and Filter for result filtration.

3. Defines Products property of the type ObservableCollection<Product> for string filtered Products based upon Criteria and Filter.

4. Defines 'Get' method to retrieve Product information from DataAccess class.

5. Defines public Command property 'FilterCommand' to execute 'Get' method.

Step 5: Open MainWindow.xaml and add TextBox, DataGrid, ListBox and TextBlocks:

<Window x:Class="WPF_Interactivity.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:WPF_Interactivity.ViewModels"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        Title="MainWindow" Height="550" Width="777.632">
    <Window.Resources>
        <vm:FilterViewModel x:Key="vmObj"></vm:FilterViewModel>
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource vmObj}}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140*"/>
            <RowDefinition Height="379*"/>
        </Grid.RowDefinitions>
        
        <TextBlock HorizontalAlignment="Left" Height="29" Margin="10,16,0,0" 
                   TextWrapping="Wrap" Text="Select the Search Criteria:"
                   VerticalAlignment="Top" Width="149"/>


        <TextBox HorizontalAlignment="Left" Height="50" Margin="381,52,0,0" 
                 TextWrapping="Wrap" Text="{Binding Filter,UpdateSourceTrigger=PropertyChanged}" 
                 VerticalAlignment="Top" Width="265"
                 Name="txtfilter">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="TextChanged">
                    <i:InvokeCommandAction Command="{Binding FilterCommand}"
                                            CommandParameter="{Binding ElementName=txtfilter,Path=Text}"></i:InvokeCommandAction>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </TextBox>

        <DataGrid Grid.Row="1" Name="dgProducts"  ItemsSource="{Binding Products}"
                  AutoGenerateColumns="True" ColumnWidth="*"/>
        
        <ListBox HorizontalAlignment="Left" Height="120" Margin="180,10,0,0" 
                 VerticalAlignment="Top" Width="120"
                 Name="lstproperties" ItemsSource="{Binding Properties}"
                  SelectedItem="{Binding Criteria,UpdateSourceTrigger=PropertyChanged}">
            
        </ListBox>
        
        <TextBlock HorizontalAlignment="Left" Height="29" Margin="381,3,0,0" 
            TextWrapping="Wrap" Text="Enter Match Value:"
            VerticalAlignment="Top" Width="265"/>

    </Grid>
</Window>

Since we need to associate the Commanding with the TextBox, we need to refer the System.Windows.Interactivity namespace in XAML. The highlighted code in the root <Window> tag contains the interactivity reference. The TextBox of name txtfilter is set with the Interaction with the TextChanged event. This is passed to the EventTrigger. In this case, when the TextChanged event is fired by the TextBox, the InvokeCommandAction will invoke the Command property, here in this case FilterCommand is passed to it. The Text entered in the TextBox will be accepted as a CommandParameter. This means that when the TextChanged event is fired on the TextBox, the FilterCommand will be invoked and this will execute the method on the ViewModel which is passed to the FilterCommand.

Execution

Run the application and the following result will be displayed:

wpf-commanding

Select Filter Criteria from the ListBox e.g. ProductName and enter some text in the TextBox. When the TextChanged event is fired, the Filtered Products will be displayed in the DataGrid as shown here:

filtered-textbox

Conclusion: System.Windows.Interactivity helps us to associate custom commanding for those WPF elements who do not have the Command property exposed. This namespace also helps developers to overcome  the code-behind paradigm for elements and implement code-less UI development. This also eliminates the need of a third party framework just for implementing custom behavior in elements.

Download the source code of this article (Github)

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Mahesh Sabnis is a DotNetCurry author and a Microsoft MVP having over two decades of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions), and Front-end technologies like Angular and React. Follow him on twitter @maheshdotnet or connect with him on LinkedIn


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by Srinivasan on Tuesday, January 13, 2015 8:39 PM
Required functionality. I keep watching your articles, really made me to think about even tiny steps. Thanks for your article.
Comment posted by Mahesh on Wednesday, January 21, 2015 10:43 AM
Thanks a lot Srinivasan.