Friday, November 5, 2021

Converting Converters

I have a WPF page containing controls that have to be enabled and disabled under complex conditions. Rather that creating an IsXXXXEnabled property for each one and try to remember to update them all as conditions change, I want to embed that functionality in XAML to separate it from the business logic in the code behind.

I came up with a ComputeConverter. This idea is so obvious that I'm sure I'm like the 10,000th person to think of it. The idea is to create a MultiValue converter that takes a parameter, substitutes the values, and uses DataTable.Compute to evaluate it, returning the result. Similar to String.Format, I use {n} as place holders. So to multiply two numbers the parameter would be "{}{0} * {1}". The starting {} tells the XAML parser that {0} is not a markup extension. String literals need to be delimited so to concatenate two strings with a hyphen the parameter would be "'{0}' + '-' + '{1}'". This parameter does not need a {} because it does not start with a { character.

In this example I have a legal document and an access mode. A witness is only needed for Affidavits and Wills and can only be entered when the access mode is Editable. At all other times, the witness name text box must be disabled.

Start a new C# Visual Studio WPF Framework project and call it ComputeConverter.

Add a new class called Converters and replace the code with this. All it does is try to substitute each value into the parameter and then evaluate the result. There is no code to look for missing or excess values, etc. If the developer gets the parameter wrong, weird things will happen.

using System;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
 
namespace ComputeConverter
{
    public class ComputeConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            String Expression = System.Convert.ToString(parameter);
            DataTable DT = new DataTable();
 
            for (int i = 0; i < values.Count(); i++)
                Expression = Expression.Replace("{" + i.ToString() + "}", values[i].ToString());
            return DT.Compute(Expression, "");
        }
 
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Now we can see the converter being used. Change the contents of MainWindow.xml to look like this. You can see we have defined a style to highlight the IsEnabled state of the text box. We have two combo boxes to allow selection of document type and access mode. The fun part is the parameter of the compute converter.

ConverterParameter="('{0}'='Affidavit' OR '{0}'='Will') AND '{1}'='Editable'">

This will return True if the first parameter is Affidavit or Will AND the second parameter is Editable. This is extremely powerful, although it's a bit slow.

<Window x:Class="ComputeConverter.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:ComputeConverter"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Compute Converter" Height="450" Width="800">
    <Window.Resources>
        <local:ComputeConverter x:Key="ComputeConverter"/>
        <Style TargetType="TextBox">
            <Setter Property="Background" Value="LightBlue"/>
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="True">
                    <Setter Property="Background" Value="White"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
 
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Document Type"/>
        <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding DocumentTypes}" SelectedItem="{Binding SelectedDocumentType}"/>
 
        <TextBlock Grid.Row="1" Grid.Column="0" Text="Access Mode"/>
        <ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding AccessModes}" SelectedItem="{Binding SelectedAccessMode}"/>
 
        <TextBlock Grid.Row="3" Grid.Column="0" Text="Witness Name"/>
        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding WitnessName}">
            <TextBox.IsEnabled>
                <MultiBinding Converter="{StaticResource ComputeConverter}" ConverterParameter="('{0}'='Affidavit' OR '{0}'='Will') AND '{1}'='Editable'">
                    <Binding Path="SelectedDocumentType"/>
                    <Binding Path="SelectedAccessMode"/>
                </MultiBinding>
            </TextBox.IsEnabled>
        </TextBox>
    </Grid>
</Window>

Time for MainWindow.xaml.cs, which contains no surprises.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
 
namespace ComputeConverter
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public List<String> DocumentTypes { get; set; } = new List<string> { "Affidavit","Contract","Deed","Lien","Will" };
        public List<String> AccessModes { get; set; } = new List<string> { "Locked", "Security", "Sealed", "Editable" };
 
        private String _SelectedAccessMode = "Locked";
        public String SelectedAccessMode
        {
            get { return _SelectedAccessMode; }
            set { SetProperty(ref _SelectedAccessMode, value); }
        }
 
        private String _SelectedDocumentType = "Deed";
        public String SelectedDocumentType
        {
            get { return _SelectedDocumentType; }
            set { SetProperty(ref _SelectedDocumentType, value); }
        }
 
        public String WitnessName { get;set;}
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

The result is that the text box is only enabled under the required conditions