Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Jeremy Davis
Jeremy Davis
Sitecore, C# and web development

Fancy paste behaviour in WPF

Composition over inheritance wins again

Published 14 February 2022

I realised recently that I've become quite used to way many web forms let you paste image data straight into a text field. The behaviour of "upload the image data, and insert the correct mark-up for the image" is a really helpful shortcut when you're editing DevOps tickets, or Stack Overflow answers. So I started wondering how easy it would be to add that to the text editing tool I use for writing these blog posts. Turns out, not too hard, because WPF has some helpful extension patterns...

The challenge

I'm typing this in a (very) simple WPF app I wrote. It's a basic text editor, which I've enhanced with a few helpful extras which make my writing process easier for me. I have shortcuts to insert common Markdown and HTML tags, just to save typing. It's got a "pick the right tags" dialog, which can look at my current blog posts and give me a pick list of tags I've used before. And it's able to fire up the Statiq generator to give me close-to-realtime previews of what I'm writing. The editor's code is pretty horrible right now - but it works for me.

Since Markdown is plain text, the bulk of the UI is a WPF TextBox that I can type into. But if you put any sort of data that isn't text onto your clipboard and try to paste it into this textbox control it won't work. The context menu option for paste will disabled and shortcut keys will do nothing. For example, if I copy an image in Paint.Net I see a greyed-out context menu:

The context menu of a WPF textbox, with the paste option greyed out

Which isn't surprising - why would a default textbox know what to do with anything that wasn't text? So the challenge is to work out how to extend this behaviour...

Composition for the win

One of the ways WPF improves upon old-style Windows Forms is that it favours a "composition before inheritance" model for adding customisations. It has a collection of extension points where you can provide composable helper objects which give new behaviour to a control. For example, in WPF we have "adorner" classes to change the way a component draws itself, rather than the old "override the Paint() method that was required in Forms. Because they're composable, they are potentially reusable across multiple controls.

It happens that WPF has one of these extension points for logical behaviours too - so that's the place to start implementing this behaviour.

A simple example

To start with this, you need to add a Nuget package to your project: Microsoft.Xaml.Behaviours.Wpf. That gives you the relevant types to implement your own reusable behaviours. You start with a class for your new behaviour:

public class PasteBehaviour : Behaviour<TextBox>
{
}

					

By making the type parameter to Behaviour a TextBox here, we're saying that this behaviour expects to work on textboxes. But you could use more generic WPF UI types here, if your behaviour doesn't require such a specific type. And with that defined, you can attach it to any relevant UI component with some XAML. For example, on my textbox:

<TextBox>
    <behaviour:Interaction.Behaviors>
        <local:PasteBehavior/>
    </behaviour:Interaction.Behaviors>
</TextBox>

					

Here the behaviour namespace maps to that Nuget package's namespace: http://schemas.microsoft.com/xaml/behaviors and local is the namespace for where the custom behaviour class is declared.

To make this type do custom pasting behaviour, it needs two key things. First is logic to attach and detach the its code from the UI element. And the second is the logic for whatever the new behaviour should actually do.

Attaching and detaching are pretty simple - there methods to overload in your behaviour. In those, you can attach and detach whatever event handlers you need:

protected override void OnAttached()
{
    base.OnAttached();

    CommandManager.AddPreviewExecutedHandler(AssociatedObject, onPreviewExecuted);
    CommandManager.AddPreviewCanExecuteHandler(AssociatedObject, onPreviewCanExecute);
}

protected override void OnDetaching()
{
    base.OnDetaching();

    CommandManager.RemovePreviewExecutedHandler(AssociatedObject, onPreviewExecuted);
    CommandManager.RemovePreviewCanExecuteHandler(AssociatedObject, onPreviewCanExecute);
}

					

In this case, the key events to handle are ones that preview the "can execute" and "executed" events for some routed commands. A routed command is the way WPF binds actions like "button clicked" or "shortcut key pressed" to UI elements. So by previewing these we can spot pasting behaviour however it's triggered, and apply our custom logic. The AssociatedObject here is whatever TextBox this Behaviour<> class has been bound to.

The logic for previewing the "can execute" event is fairly simple. If the command being previewed is "Paste" we need to override the logic for "can this control accept the clipboard data format that's present" and allow it to accept image data. (The default control we're extending will already handle text here) We can do that with something like:

private void onPreviewCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    if (e.Command == ApplicationCommands.Paste)
    {
        e.CanExecute = Clipboard.ContainsImage();
        e.Handled = Clipboard.ContainsImage();
    }
}

					

By setting the value of CanExecute we're extending this "can the control accept the data" logic. And by setting Handled we tell the runtime that we made a change. And with that in place, we should see that the context menu "paste" entry our textbox will not be greyed out if we have bitmap data on the clipboard.

Next the code needs to extend the actual "data is being pasted" event - which the attachment logic above is sending to a onPreviewExecuted method in this behaviour object. As before, that needs to check that the command being processed is "paste" and that the clipboard data is an image. But if those tests pass, it can get on and perform its custom functions, before again setting Handled true to show it did stuff with the event. And as noted above, the AssociatedObject here (whatever we've bound this behaviour logic too) is a TextBox so we can do operations on the text it contains:

private void onPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
{
    if (e.Command == ApplicationCommands.Paste)
    {
        if (Clipboard.ContainsImage())
        {
            // work out a disk filename and web url for our new image
            // save the image to disk

            var markup = $"![Alt text here](/web-path-to-file/goes-here.png)";

            AssociatedObject.Text = AssociatedObject.Text.Insert(AssociatedObject.SelectionStart, markup);

            e.Handled = true;
        }
    }
}

					

So the outcome of all of this is that the custom code written above "previews" the events sent to the textbox. We get to run the extra code before the default behaviour of the textbox runs, and hence we can extend its features. So now when I copy some image data from Paint.Net, I see this:

The context menu of a WPF textbox, with the paste option enabled

And if I click "Paste" or hit Ctrl-V I now get my link inserted:

I've skipped how the clipboard data ends up as an image file with a URL here. But that's not really relevant to the basic pattern for extending paste behaviour. (And saving images is fairly easily googleable) And what's above is pretty much all the code required to allow doing this a data conversion to paste something that's not text into a texbox.

If you'd like to fiddle with this yourself, there's a zip of the basic code above you can grab if you want...