Thursday, July 30, 2020

Undo and Redo with Postsharp

This is a bit scary. If the users knew we had access to this functionality they would camp out on our lawns until we promised to implement it. I'm talking about undo and redo on screens. With PostSharp, this is a breeze. I'm going to walk you through it.

We are going to create a simple screen that displays a vendor and a list of vendor addresses. We will include three bits of PostSharp functionality, two of which I have discussed in earlier blog entries.
The PostSharp documentation for Undo/Redo is here.

Start a new Visual Studio 2019 WPF .Net Core project using C# and call it UndoRedo. Use NuGet to add PostSharp.Patterns.Model and PostSharp.Patterns.Xaml. Add your license to the project.

We can start with the code-behind. We define a Vendor and a VendorAddress class like this. The only attribute we haven't covered in earlier posts is [Recordable]. This indicates that changes to the class properties should be recorded and undoable. We can use the [NotRecorded] attribute on an individual property to indicate that changes should not be included in the change log.

    [NotifyPropertyChanged]
    [Recordable]
    public class Vendor
    {
        public string VendorCode { get; set; }
        public string VendorName { get; set; }

        [Child]
        public IList<VendorAddress> VendorAddresses { get; private set; }

        public Vendor()
        {
            this.VendorAddresses = new AdvisableCollection<VendorAddress>();
        }
    }

    [NotifyPropertyChanged]
    [Recordable]
    public class VendorAddress
    {
        // There is a bug in PostSharp when undoing the delete of a member of an AdvisableCollection when that member has a [Parent] property
        // We can fix it by separating the [Parent] from the property. They are working on it. Bug #27256
        [Parent]
        private Vendor _vendor;

        public Vendor vendor
        {
            get { return _vendor; }
            private set { _vendor = value; }
        }

        public string VendorStreetAddress { get; set; }
        public string VendorCity { get; set; }
        public string VendorState { get; set; }
        public string VendorZipCode { get; set; }
    }

We need to declare a Vendor property in MainWindow and initialize it in the constructor. We probably need to start with an empty change history whenever we load or save a vendor which we do by clearing the recorder. In this example we do it in the MainWindow constructor, but we would also want to do it after any load or save.

    public partial class MainWindow : Window
    {
        public Vendor vendor { get; set; }
           
        public MainWindow()
        {
            RecordingServices.DefaultRecorder.Clear();
            vendor = new Vendor();
            InitializeComponent();
        }
    }

As you can see the undo/redo logic is encapsulated in a using, two decorations, and one line of code. Let's take a look at the XAML. It's pretty much what you expect. The two custom buttons provide the user interface for undo/redo. I made them bold below.

<Window x:Class="UndoRedo.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:model="clr-namespace:PostSharp.Patterns.Model.Controls;assembly=PostSharp.Patterns.Xaml"
        xmlns:local="clr-namespace:UndoRedo"
        mc:Ignorable="d"
        Title="Vendor Maintenance" Height="450" Width="800"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="Vendor Code:" Margin="10,0,0,0"/>
            <TextBox Grid.Column="1" Text="{Binding vendor.VendorCode}" Width="100" MaxLength="6" Margin="10,0,0,0"/>
            <TextBlock Grid.Column="2" Text="Vendor Name:" Margin="20,0,0,0"/>
            <TextBox Grid.Column="3" Text="{Binding vendor.VendorName}" Width="400" MaxHeight="100" Margin="10,0,0,0"/>
            <model:UndoButton Grid.Column="5"/>
            <model:RedoButton Grid.Column="6"/>
        </Grid>
        <DataGrid Grid.Row="1" ItemsSource="{Binding vendor.VendorAddresses}" CanUserAddRows="True" CanUserDeleteRows="True" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Street Address" Binding="{Binding VendorStreetAddress}" Width="4*"/>
                <DataGridTextColumn Header="City" Binding="{Binding VendorCity}" Width="*"/>
                <DataGridTextColumn Header="State" Binding="{Binding VendorState}" Width="50"/>
                <DataGridTextColumn Header="Zip Code" Binding="{Binding VendorZipCode}" Width="100"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Let's take it out for a spin. Run the program and enter a vendor number, name, and address something like this.


Click the Undo button a few times (you can see it has become enabled) so the screen looks something like this.


And hit redo until your data is back!


Now select the address line and hit delete. As you expect, it is deleted.


Click the Undo button and it comes back!


If you look in the VendorAddress class you can see I have added a comment that mentions a bug I found.

This is the first bug I have found in PostSharp. I zipped up the solution and submitted a question to them. They got back the next day and confirmed they can reproduce the error, gave me a workaround, and they will fix it. This is pretty impressive.

I was thinking about unit testing the undo/redo functionality and was worried that the undo/redo buttons have no exposure in the code behind. However, you could access the recorder object directly and execute undo/redo methods on it.

Wednesday, July 22, 2020

PostSharp and Dependency Properties

Writing dependency properties is another one of those boilerplate coding exercises. Fortunately we don't write many dependency properties unless we are WPF control authors, but it does get repetitive and easy to make mistakes.

Here's an example of one...


public static readonly DependencyProperty ColumnsProperty =
            DependencyProperty.RegisterAttached("Columns", typeof(ObservableCollection<DataGridColumn>), typeof(FooterGrid), new UIPropertyMetadata(new ObservableCollection<DataGridColumn>(), ColumnsPropertyChanged));

public static int GetColumnSpan(DependencyObject obj)
{
    return (int)obj.GetValue(ColumnSpanProperty);
}

public static void SetColumnSpan(DependencyObject obj, int value)
{
    obj.SetValue(ColumnSpanProperty, value);
}


With PostSharp this is reduced to... 

 [DependencyProperty]
 public ObservableCollection<DataGridColumn> Columns { get; set; }

PostSharp also simplifies other aspects of dependency properties too. Let's walk through some.

Start a new WPF project in Visual Studio. I am using 2019, C#, .Net Framework. Call the project PostSharpDependencyProperty. Use NuGet to add PostSharp.Patterns.Xaml and add your license information to the project.

Add a new class called DigitOnlyTextBox. This will subclass the TextBox and add a boolean dependency property called OnlyAllowDigits which will control whether or not the TextBox will accept non-digit characters. Make it subclass TextBox.

using System.Text.RegularExpressions;
using System.Windows.Controls;
using PostSharp.Patterns.Xaml;

namespace PostSharpDependencyProperty
{
    class DigitOnlyTextBox:TextBox
    {
        [DependencyProperty]
        public bool OnlyAllowDigits { get; set; }
    }
}

Defining that dependency property was pretty painless.

When the text changes, we check the new property to decide if we want to strip the non-digits out of the new text. Add this code...

using System.Text.RegularExpressions;
using System.Windows.Controls;
using PostSharp.Patterns.Xaml;

namespace PostSharpDependencyProperty
{
    class DigitOnlyTextBox:TextBox
    {
        [DependencyProperty]
        public bool OnlyAllowDigits { get; set; }

        public DigitOnlyTextBox()
        {
            this.TextChanged += DigitOnlyTextBox_TextChanged;
        }

        ~DigitOnlyTextBox()
        {
            this.TextChanged -= DigitOnlyTextBox_TextChanged;
        }

        private void DigitOnlyTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            ApplyOnlyAllowDigits();
        }

        private void ApplyOnlyAllowDigits()
        {
            if (OnlyAllowDigits)
                this.Text = StripNonDigits(this.Text);
        }

        private string StripNonDigits(string sIn)
        {
            return Regex.Replace(sIn, "[^0-9]", "");
        }
    }
}

Our last requirement is to strip out digits when the AllowOnlyDigits property is set true. Thank goodness we didn't put all that code directly in DigitOnlyTextBox_TextChanged.Writing the callback code is simply a matter writing a method with a specific name. Add this method. Note the name is On<PropertyName>Changed.

        private void OnOnlyAllowDigitsChanged()
        {
            ApplyOnlyAllowDigits();
        }
Here is some XAML for MainWindow.xaml that will instantiate and exercise our new text box.

<Window x:Class="PostSharpDependencyProperty.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:PostSharpDependencyProperty"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <StackPanel Orientation="Vertical">
        <local:DigitOnlyTextBox Width="100" Height="20" OnlyAllowDigits="{Binding OnlyAllowDigits}"/>
        <CheckBox Content="Only allow digits: "  IsChecked="{Binding OnlyAllowDigits}"/>
    </StackPanel>
</Window>

The code-behind needs a OnlyAllowDigits property defined. Note we are using the PostSharp [NotifyPropertyChanged] decoration.

using PostSharp.Patterns.Model;
using System.Windows;

namespace PostSharpDependencyProperty
{
    [NotifyPropertyChanged]
    public partial class MainWindow : Window
    {
        public bool OnlyAllowDigits { get; set; }

        public MainWindow()
        {
            OnlyAllowDigits = true;
            InitializeComponent();
        }
    }
}

The TextBox starts by only allowing digits. Make sure you cannot enter non-digits. 
Now clear the checkbox and ensure you can enter non-digits.
Check the checkbox again and see that the non-digits are removed.