Friday, June 18, 2021

Adding a converter parameter to a custom binding markup extension

My earlier blog entry about a markup extension to simplify binding in an Infragistics TemplateField is good as far as it goes, but I wanted to add the ability to specify a Converter. This turned out to be far more complex than I thought it would be.

I thought it would be a matter of specifying a Converter property and adding it to the Binding. But when you add a StaticResource to the parameter list of a markup extension, it causes the sequence of events to change and stuff gets evaluated before you are ready. I want to thank this codeproject article for explaining it so well.

Let's take the earlier project and modify it the way I thought it would work. Open the XamDataGridTemplateField solution from my earlier entry. Add a new class called Converters and add a trivial converter to it like this.

using System;
using System.Globalization;
using System.Windows.Data;
 
namespace XamDataGridTemplatedField
{
    class FontSizeConverter :IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
    }
}

Now open the TemplateBindings class and make these changes.

  • Replace the Path member with a property
  • Add a Converter property of type IValueConverter
  • Add a default constructor
        public String Path { get; set; }
        public IValueConverter Converter { get; set; }
 
        public TemplateBinding()
        { }

  • In CreateBinding, assign the converter.

            b.Converter = Converter;

Open MainWindow.xaml and make these changes... 

Add a resources section referencing the new converter

    <Window.Resources>
        <local:FontSizeConverter x:Key="FontSizeConverter"/>
    </Window.Resources>

and modify the FontSize binding in the two TextBoxes.

<TextBlock Text="{igEditors:TemplateEditorValueBinding}" FontSize="{local:TemplateBinding Path=fontSize, Converter={StaticResource FontSizeConverter}}"/>

If you run the application now you will get an error.


If you debug you will see that referring to the StaticResource in the binding causes InitializeComponent to get called earlier than before which ends up raising this exception. The article in CodeProject explains how to create a "singleton" version of the converter that can be referenced directly in the binding. Let's do that now.

Change the converter to make it inherit from MarkupExtension, defer creation of the object until ProvideValue, and implement a limited singleton pattern. The important thing is that we are making the converter a Markup Extension in its own right so we can reference it directly. I don't know why this is not standard practice.

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;
 
namespace XamDataGridTemplatedField
{
    class FontSizeConverter : MarkupExtension, IValueConverter
    {
        private static FontSizeConverter _converter;
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return null;
            else
                return
value;
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
 
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (_converter == null)
            {
                _converter = new FontSizeConverter();
            }
            return _converter;
        }
    }
}
 
Now open MainWindow.xaml, remove the StaticResource and alter the FontSize bindings to reference the converter directly.

<TextBlock Text="{igEditors:TemplateEditorValueBinding}" FontSize="{local:TemplateBinding Path=fontSize, Converter={local:FontSizeConverter}}"/>

If you run the application now you will see the binding was successful. Put a break point in the converter's Convert method to see that it is being called.


 Note: We added the extra constructor so we could bind with two different syntaxes.

<TextBlock Text="{igEditors:TemplateEditorValueBinding}" FontSize="{local:TemplateBinding Path=fontSize, Converter={local:FontSizeConverter}}"/>

<TextBlock Text="{igEditors:TemplateEditorValueBinding}" FontSize="{local:TemplateBinding fontSize, Converter={local:FontSizeConverter}}"/>


Tuesday, June 15, 2021

TextBlock Runs in XAML and code

You know how you're reading an article and it explains how to do something cool, and you think "That's interesting, I'll remember you can do that" but you don't bother to remember the details? I did that many years ago when I read that TextBlocks can have Runs. They are sequences of characters with different characteristics in the same TextBlock. You can do the same thing with rtf but it's very complicated so I didn't really pay much attention at the time.

I came across a requirement recently that could be solved by making parts of the text in a TextBlock invisible at run time so I created the runs in code. Here's an example of doing it in XAML and then I'll do it again in code.

Start a Visual Studio 2019 WPF Application (Core) project in C#. Call it Runs. We will add a TextBlock and two combo boxes to the MainWindow. The TextBlock will have two runs and the combo boxes will control the font size in the two runs. The XAML looks like this...

<Window x:Class="Runs.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:Runs"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <TextBlock>
            <TextBlock.Inlines>
                <Run Text="Hello" FontSize="{Binding FontSize1}"/>
                <Run Text=" "/>
                <Run Text="World" FontSize="{Binding FontSize2}"/>
            </TextBlock.Inlines>
        </TextBlock>
        <StackPanel Orientation="Horizontal">
            <ComboBox ItemsSource="{Binding FontSizes}" SelectedItem="{Binding FontSize1}" Width="50"/>
            <ComboBox ItemsSource="{Binding FontSizes}" SelectedItem="{Binding FontSize2}" Width="50"/>
        </StackPanel>
    </StackPanel>
</Window>

The code behind looks like this...

using System;
using System.ComponentModel;
using System.Windows;
 
namespace Runs
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public double[] FontSizes
        {
            get { return new double[] { 8, 10, 12, 14, 16, 20 }; }
        }
 
        private double _FontSize1 = 12;
        public double FontSize1
        {
            get { return _FontSize1; }
            set
            {
                _FontSize1 = value;
                NotifyPropertyChanged();
            }
        }
 
        private double _FontSize2 = 12;
        public double FontSize2
        {
            get { return _FontSize2; }
            set
            {
                _FontSize2 = value;
                NotifyPropertyChanged();
            }
        }
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String name = "")
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}

When you run the application you can control the font size in the two halves of the text block.

 


Here is the same thing done in code...

<Window x:Class="Runs.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:Runs"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <TextBlock Initialized="TextBlock_Initialized"/>
        <StackPanel Orientation="Horizontal">
            <ComboBox ItemsSource="{Binding FontSizes}" SelectedItem="{Binding FontSize1}" Width="50"/>
            <ComboBox ItemsSource="{Binding FontSizes}" SelectedItem="{Binding FontSize2}" Width="50"/>
        </StackPanel>
    </StackPanel>
</Window>

---------------------------------------------

 using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
 
namespace Runs
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public double[] FontSizes
        {
            get { return new double[] { 8, 10, 12, 14, 16, 20 }; }
        }
 
        private double _FontSize1 = 12;
        public double FontSize1
        {
            get { return _FontSize1; }
            set
            {
                _FontSize1 = value;
                NotifyPropertyChanged();
            }
        }
 
        private double _FontSize2 = 12;
        public double FontSize2
        {
            get { return _FontSize2; }
            set
            {
                _FontSize2 = value;
                NotifyPropertyChanged();
            }
        }
 
        public MainWindow()
        {      
            InitializeComponent();
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String name = "")
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
 
        private void TextBlock_Initialized(object sender, EventArgs e)
        {
            TextBlock tb = (TextBlock)sender;
            Run r1 = new Run("Hello");
            Run r2 = new Run(" ");
            Run r3 = new Run("World");
 
            Binding b1 = new Binding("FontSize1") { Source = this };
            Binding b3 = new Binding("FontSize2") { Source = this };
            r1.SetBinding(Run.FontSizeProperty, b1);
            r3.SetBinding(Run.FontSizeProperty, b3);
 
            tb.Inlines.Clear();
            tb.Inlines.Add(r1);
            tb.Inlines.Add(r2);
            tb.Inlines.Add(r3);
        }
    }
}