1

The Situation

I have a .NET Framework 4.8 Microsoft-Visio-like WPF application that enables the user to place controls on a canvas and modify them. I keep references to all these elements in an ObservableCollection in my ViewModel which is then set as ItemsSource on my Canvas.

To save the controls and various other aspects of the current project I'm serializing everything to JSON using the Newtonsoft serializer and write the result to a file. For this, I have created a dedicated model for serialization which I populate in the Save() method. At one point, I directly serialize the UI elements as content into another object like so:

var designerElements = CanvasElements.Select(element =>
{
    var wrapper = element.FindVisualAncestor<DesignerElementWrapper>();

    var designerElement = new DesignerElement
    {
        Size = new Size(wrapper.Width, wrapper.Height),
        Position = new Point(Canvas.GetLeft(wrapper), Canvas.GetTop(wrapper)),
        ContentJson = JsonConvert.SerializeObject(element, settings),
        ContentType = element.GetType(),
        <...>
    };

    return designerElement;
}

Currently, the saving/serialization work is done on the UI thread because this was convenient and worked well for relatively few objects.

The Problem

Now with the projects growing larger and larger saving the project, especially serialization, takes quite a while and starts significantly impacting (freezing) the UI.

I'd now like to relay the serialization or ideally all the saving work to a thread pool thread:

var designerElements = Task.WhenAll(CanvasElements.Select(async element =>
{
    <...>

    var contentJson = await Task.Run(() => JsonConvert.SerializeObject(element, settings));
    var designerElement = new DesignerElement
    {
        <...>
        ContentJson = contentJson,
        <...>
    };

    <...>
}

// Then serialize designerElements and write result to file

or

Task.Run(() =>
{
    // Gather data, project data into serializable model,
    // serialize model (incl. UI elements) and write result to file
}

The problem is, that my UI elements obviously belong to the UI thread and I now get InvalidOperationExceptions when serializing, due to the background thread trying to access objects owned by the UI thread.

The Question

How do I deal with this in a thread-safe manner without blocking the UI thread?

toogeneric
  • 53
  • 6
  • 2
    You should not have UIElements in a view model in the first place. The view model should instead deal with abstract representations of shapes and the like, which are visualized by elements in DataTemplates in the view. See this answer to get an idea: https://stackoverflow.com/a/22325266/1136211 – Clemens Sep 08 '21 at 13:41
  • Thanks for the great advice @Clemens! This means I'd have to more or less create an abstraction for each type of custom control that I implement. Given that my ItemsControl has a ContentControl as its ItemTemplate to act as a wrapper for the actual custom control (for resizing and stuff), I'd then have to use a ContentTemplateSelector to match the templates to their abstractions, correct? – toogeneric Sep 08 '21 at 14:02
  • 1
    No, you could have DataTemplates automatically selected by their DataType. When DataType matches a view model type, the corresponding DataTemplate is selected automatically. The DataTemplate must be declared as a resource without `x:Key` in an accessible ResourceDictionary. – Clemens Sep 08 '21 at 14:11
  • I would like to understand if the delays you're seeing are in the serialization or the actual saving of the JSON? – Enigmativity Sep 08 '21 at 22:56
  • 1
    Also, you should never access a UI element from a non-UI thread. So it really rules out using tasks to access your controls. What I usually do is generate an intermediate object (if a non-UI object doesn't exist already) on the UI thread and then pass that to the task to avoid accessing the UI. In other words, do the minimal amount of work necessary on the UI thread and then do the remaining work on in a Task. – Enigmativity Sep 08 '21 at 22:59
  • Alright @Clemens, could've thought of that myself. Thanks for pointing it out! – toogeneric Sep 09 '21 at 08:27
  • @Enigmativity I was also thinking about doing that, seemed like a lot of overhead to me tho. Anyway, thank you too for your advice. To answer your first comment: According to dotTrace the main impact definitely comes from serialization. Writing the JSON to file only takes a couple of ms. – toogeneric Sep 09 '21 at 08:36

0 Answers0