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)
{
}
}
}
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>
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();
}
}
}
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();
}
}
}
Hiding the 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 |
<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 (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;
}
- 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();
}
}
}
public MyMultiColumnEditor()
{
this.SelectionChanged += MyMultiColumnEditor_OnSelectionChanged;
this.Initialized += MyMultiColumnEditor_Initialized;
}
~MyMultiColumnEditor()
{
this.Initialized -= MyMultiColumnEditor_Initialized;
this.SelectionChanged -= MyMultiColumnEditor_OnSelectionChanged;
}
<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>
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>
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.