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.
- NotifyPropertyChanged
- Aggregatable
- Undo/Redo
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>
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.