Tuesday, November 12, 2019

Disabling rows in Infragistics MultiColumnEditors

Now that my design team has realized that in WPF we can disable rows in drop down lists without the developers threatening to resign, they are starting to find all sorts of interesting places to do this. I've recently covered simple drop down lists, drop down lists in data grids, and drop down lists in Infragistics data grids, each of which is slightly different.

Yesterday I was asked to disable rows in an Infragistics XamMultiColumnComboEditor. I went through the usual Google searches to try to find the correct style TargetType because Infragistics still doesn't understand that this should be front and foremost in the documentation. Eventually I came across this post which prevents the click event from selecting a disabled row but doesn't change how the disabled row looks nor does is prevent the user from selecting a disabled row by cursoring down and hitting enter. So that was a fail.

I then looked through the Infragistics sample download (which is well worth looking at) but although the other dropdowns have examples of disabling rows, the sample for the Multi Column Combo Editor conspicuously does not. Everything I've seen so far tells me Infragistics forgot to add this functionality to this control.

So this is one way to do it. I subclassed the XAMMultiColumnComboEditor to give me somewhere to put the SelectedChanged handler. I defined two dependency properties "IsEnabledPath" and "IsDisabledPath". The idea is the developer will specify one or the other path, but not both. If one is specified I will assume there is a boolean property or a data column with that name and I will enable or disable rows accordingly. I will also construct styles and triggers appropriately. I will not support run-time changes to these properties but it could be done.

Start a new WPF C# project called DisableMultiColumnComboItem using any framework after 3.5.

Add references to Infragistics Editors like this.


Add a class to the project called MyMultiColumnEditor. It will derive from XamMultiColumnComboEditor like this. All we are doing at this stage is adding the dependency properties and a SelectionChanged event handler. We will finish it later.


using System.Data;
using System.Reflection;
using Infragistics.Controls.Editors;

namespace DisableMultiColumnComboItem
{
    class MyMultiColumnEditor : XamMultiColumnComboEditor
    {
        private object LastValue = null;

        public static readonly DependencyProperty IsEnabledPathProperty = DependencyProperty.Register("IsEnabledPath", typeof(string), typeof(MyMultiColumnEditor), new PropertyMetadata(""));
        public static readonly DependencyProperty IsDisabledPathProperty = DependencyProperty.Register("IsDisabledPath", typeof(string), typeof(MyMultiColumnEditor), new PropertyMetadata(""));

        public string IsEnabledPath
        {
            get { return GetValue(IsEnabledPathProperty).ToString(); }
            set { SetValue(IsEnabledPathProperty, value); }
        }

        public string IsDisabledPath
        {
            get { return GetValue(IsDisabledPathProperty).ToString(); }
            set { SetValue(IsDisabledPathProperty, value); }
        }

        public MyMultiColumnEditor()
        {
            this.SelectionChanged += MyMultiColumnEditor_OnSelectionChanged;
        }

        ~MyMultiColumnEditor()
        {
            this.SelectionChanged -= MyMultiColumnEditor_OnSelectionChanged;
        }

        private void MyMultiColumnEditor_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
        }

    }
}

Now open MainWindow.xaml. We are going to declare three XamMultiColumnComboEditors. The first will simple hide disabled rows, the second will disable them while bound to a collection of objects, the third will disabled them while bound to a dataview.

Option one is the simplest but it confuses the more feeble-minded users so it's not an option for us. It just requires a simple converter to collapse the disabled rows. Options two and three demonstrate common binding scenarios.

We will start with option one and add the others later. It doesn't actually require any special functionality - it works perfectly well as a XamMultiColumnComboEditor.

<Window x:Class="DisableMultiColumnComboItem.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:ig="http://schemas.infragistics.com/xaml"
        xmlns:local="clr-namespace:DisableMultiColumnComboItem"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:BooleanToVisibilityConverter x:Key="VisibilityConverter"/>
    </Window.Resources>
    <StackPanel Orientation="Horizontal">
        <local:MyMultiColumnEditor ItemsSource="{Binding Things}" Height="20" AutoGenerateColumns="False" SelectedItem="{Binding SelectedThing}" Width="200">
            <ig:XamMultiColumnComboEditor.Resources>
                <Style  TargetType="{x:Type ig:ComboCellControl}">
                    <Setter Property="Visibility" Value="{Binding IsEnabled, Converter={StaticResource VisibilityConverter}}"/>
                </Style>
            </ig:XamMultiColumnComboEditor.Resources>
            <ig:XamMultiColumnComboEditor.Columns>
                <ig:TextComboColumn HeaderText="Name" Key="Name"/>
                <ig:TextComboColumn HeaderText="Description" Key="Description"/>
            </ig:XamMultiColumnComboEditor.Columns>
        </local:MyMultiColumnEditor>
    </StackPanel>
</Window>

The code behind looks like this.

using System.Collections.Generic;
using System.Windows;
using System.Data;

namespace DisableMultiColumnComboItem
{
    public class cThing
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public bool IsEnabled { get; set; }
    }

    public partial class MainWindow : Window
    {
        public cThing SelectedThing { get; set; }

        public List<cThing> Things { get; set; } = new List<cThing>()
        {
            new cThing() { Name = "Show me", Description="I am seen",  IsEnabled = true },
            new cThing() { Name = "Hide me", Description="I am hidden", IsEnabled = false },
            new cThing() { Name = "Show me too", Description = "I am shown", IsEnabled = true }
        };

        public MainWindow()
        {
            SelectedThing = Things[1];
            InitializeComponent();
        }    
    }
}

You will also need to add a class to the project called Converters and add a simple converter

using System;
using System.Globalization;

namespace DisableMultiColumnComboItem
{
    public class BooleanToVisibilityConverter : System.Windows.Data.IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if ((bool)value)
                return System.Windows.Visibility.Visible;
            else
                return System.Windows.Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

}

If you run this example you can see the original value "Hide Me" has IsEnabled=False and does not show in the drop down list although the original value is correctly shown in the text box part. As soon as you select a different value you cannot get back to the old, disabled value. This is all functionally correct. Unfortunately this can confuse people. 


Hiding the disabled rows

Let's add Infragistic's suggestion for disabling rows. Add a new XamMultiColumnComboEditor to the XAML. This simply binds the IsEnabled property of the ComboCellControl. This prevents mouse clicks but doesn't make the row look disabled and doesn't prevent the user using the keyboard to select disabled rows.

        <local:MyMultiColumnEditor ItemsSource="{Binding Things}" Height="20" AutoGenerateColumns="False" SelectedItem="{Binding SelectedThing}" Width="200">
            <ig:XamMultiColumnComboEditor.Resources>
                <Style  TargetType="{x:Type ig:ComboCellControl}">
                    <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
                </Style>
            </ig:XamMultiColumnComboEditor.Resources>
            <ig:XamMultiColumnComboEditor.Columns>
                <ig:TextComboColumn HeaderText="Name" Key="Name"/>
                <ig:TextComboColumn HeaderText="Description" Key="Description"/>
            </ig:XamMultiColumnComboEditor.Columns>
        </local:MyMultiColumnEditor>

Infragistics solution prevents mouse clicks but that's all
 We can easily make disabled rows look disabled with a trigger.

        <local:MyMultiColumnEditor ItemsSource="{Binding Things}" Height="20" AutoGenerateColumns="False" SelectedItem="{Binding SelectedThing}" Width="200">
            <ig:XamMultiColumnComboEditor.Resources>
                <Style  TargetType="{x:Type ig:ComboCellControl}">
                    <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
                    <Setter Property="Foreground" Value="Black"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsEnabled}" Value="False">
                            <Setter Property="Foreground" Value="DarkGray"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ig:XamMultiColumnComboEditor.Resources>
            <ig:XamMultiColumnComboEditor.Columns>
                <ig:TextComboColumn HeaderText="Name" Key="Name"/>
                <ig:TextComboColumn HeaderText="Description" Key="Description"/>
            </ig:XamMultiColumnComboEditor.Columns>
        </local:MyMultiColumnEditor>


A trigger makes disabled rows look disabled
But the user can still use the cursor and enter keys to select an disabled row. Probably because the solution offered by Infragistics targets the cell and not the row.

We can monitor the SelectedChanged event to revert to the last good value if the user attempts to select a disabled row. The algorithm I'm about to propose only works for single selection combo boxes.

In MyMultiComboEditor.cs add this code to MyMultiColumnEditor_OnSelectionChanged. It checks to see if any of the added rows are disabled (should only be one) and, if so, sets the selected item to the last known good value. If the control was initialized with a disabled row, this could be the last known good value.

            bool IsEnabled = true;
            XamMultiColumnComboEditor mcce = sender as XamMultiColumnComboEditor;

            foreach (object o in e.AddedItems)
            {
                if (!IsSelectionEnabled(o)) IsEnabled = false;
            }

            if ((IsEnabled || LastValue == null) && e.AddedItems.Count > 0) LastValue = e.AddedItems[0];

            if (!IsEnabled && LastValue != null)
            {
                mcce.SelectedItem = LastValue;
            }

We need a function that will use Reflection to determine if an object has a IsEnabled or IsDisabled property. I called it IsSelectionEnabled.

        private bool IsSelectionEnabled(object o)
        {
            // Is there an IsEnabled or IsDisabled property?
            PropertyInfo pi = null;
            bool IsEnabled = false;

            if (IsEnabledPath == "" && IsDisabledPath == "") return true;

            if (pi == null)
            {
                pi = o.GetType().GetProperty(IsEnabledPath);
                if (pi != null && pi.PropertyType == typeof(bool))
                    IsEnabled = (bool)pi.GetValue(o);
            }
            if (pi == null)
            {
                pi = o.GetType().GetProperty(IsDisabledPath);
                if (pi != null && pi.PropertyType == typeof(bool))
                    IsEnabled = !(bool)pi.GetValue(o);
            }
            return IsEnabled;

        }

Now let's take that XAML style out and build it dynamically based on how the user specified the IsEnabledPath and IsDisabledPath properties. Create an Initialized event handler like this. It does the following...

  • Binds IsEnabled to the IsEnabledPath or, via a converter, to the IsDisabledPath property.
  • Sets Foreground Black
  • Creates a trigger to set the Foreground DarkGray if IsEnabled is false

        private void MyMultiColumnEditor_Initialized(object sender, EventArgs e)
        {
            Style s = new Style(typeof(ComboCellControl));
            Binding b = null;

            if (IsEnabledPath == "" && IsDisabledPath == "") return;
            if (IsEnabledPath != "") b = new Binding(IsEnabledPath);
            if (IsDisabledPath != "")
            {
                b = new Binding(IsDisabledPath);
                b.Converter = new NotConverter();
            }
            s.Setters.Add(new Setter(IsEnabledProperty, b));
            s.Setters.Add(new Setter(ForegroundProperty, new SolidColorBrush(Colors.Black)));

            Trigger t = new Trigger();
            t.Property = IsEnabledProperty;
            t.Value = false;
            t.Setters.Add(new Setter(ForegroundProperty, new SolidColorBrush(Colors.DarkGray)));
            s.Triggers.Add(t);

            Resources.Add(typeof(ComboCellControl), s);
        }

        private class NotConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                try
                {
                    if (!(value is bool)) return true;
                    return !(bool)value;
                }
                catch
                {
                    return false;
                }
            }

            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }


Add the following lines to the constructor and finalizer so they look like this.

        public MyMultiColumnEditor()
        {
            this.SelectionChanged += MyMultiColumnEditor_OnSelectionChanged;
            this.Initialized += MyMultiColumnEditor_Initialized;
        }

        ~MyMultiColumnEditor()
        {
            this.Initialized -= MyMultiColumnEditor_Initialized;
            this.SelectionChanged -= MyMultiColumnEditor_OnSelectionChanged;
        }


Now, in the XAML, replace the second MultiLineComboEditor with this. You can see we have removed the Resource and added an IsEnabledPath attribute.

        <local:MyMultiColumnEditor ItemsSource="{Binding Things}" Height="20" AutoGenerateColumns="False" SelectedItem="{Binding SelectedThing}" Width="200" IsEnabledPath="IsEnabled">
            <ig:XamMultiColumnComboEditor.Columns>
                <ig:TextComboColumn HeaderText="Name" Key="Name"/>
                <ig:TextComboColumn HeaderText="Description" Key="Description"/>
            </ig:XamMultiColumnComboEditor.Columns>
        </local:MyMultiColumnEditor>


If the user selects an enabled value, then uses the cursor to select a disabled value, the old enabled value is left in place.

With a little work we can also support controls that are bound to data tables. Add the data table properties and initialization code to the code behind in Window.xaml.cs.

        public DataRowView SelectedDR { get; set; }
        public DataTable dt { get; set; } = new DataTable();

        public MainWindow()
        {
            SelectedThing = Things[1];

            dt.Columns.Add(new DataColumn("Name", System.Type.GetType("System.String")));
            dt.Columns.Add(new DataColumn("Description", System.Type.GetType("System.String")));
            dt.Columns.Add(new DataColumn("IsEnabled", System.Type.GetType("System.Boolean")));

            dt.Rows.Add("Show me", "I am seen", true);
            dt.Rows.Add("Hide me", "I am hidden", false);
            dt.Rows.Add("Show me too", "I am shown", true);

            SelectedDR = dt.DefaultView[1];
            InitializeComponent();
        }    

Add a third combobox to our XAML. It is bound to our new datatable property but is otherwise the same.

        <local:MyMultiColumnEditor ItemsSource="{Binding dt.DefaultView}" Height="20" AutoGenerateColumns="False" SelectedItem="{Binding SelectedDR}" Width="200" IsEnabledPath="IsEnabled"
            <ig:XamMultiColumnComboEditor.Columns>
                <ig:TextComboColumn HeaderText="Name" Key="Name"/>
                <ig:TextComboColumn HeaderText="Description" Key="Description"/>
            </ig:XamMultiColumnComboEditor.Columns>
        </local:MyMultiColumnEditor>

The IsSelectionEnabled function needs to support IsEnable and IsDisabled columns in a datatable so change it to look like this.

        private bool IsSelectionEnabled(object o)
        {
            // Is there an IsEnabled or IsDisabled property?
            PropertyInfo pi = null;
            bool IsEnabled = false;


            if (IsEnabledPath == "" && IsDisabledPath == "") return true;

            if (pi == null)
            {
                pi = o.GetType().GetProperty(IsEnabledPath);
                if (pi != null && pi.PropertyType == typeof(bool))
                    IsEnabled = (bool)pi.GetValue(o);
            }
            if (pi == null)
            {
                pi = o.GetType().GetProperty(IsDisabledPath);
                if (pi != null && pi.PropertyType == typeof(bool))
                    IsEnabled = !(bool)pi.GetValue(o);
            }
            if (pi == null)
            {
                pi = o.GetType().GetProperty("Row");
                if (pi != null && pi.PropertyType == typeof(DataRow))
                {
                    DataRow dr = (DataRow)pi.GetValue(o);
                    if (dr.Table.Columns.Contains(IsEnabledPath)) IsEnabled = (bool)dr[IsEnabledPath];
                    if (dr.Table.Columns.Contains(IsDisabledPath)) IsEnabled = !(bool)dr[IsDisabledPath];
                }
            }
            return IsEnabled;
        }
 The third combo box looks and acts like the second, but is bound to a datatable.