MVVM Layout Serialization

Docking/MDI for WPF Forum

Posted 2 years ago by Justin Klein
Version: 16.1.0635
Avatar

I'm attempting to implement layout serialization, without breaking MVVM.  I've got an implementation that's *mostly* working, but can't quite get all the nuts and bolts in place, so I wonder if I'm barking up the wrong tree.  The idea is to use a Behavior to subscribe to any event that will change the layout, and provide the serialized layout string as a DependencyProperty.  Here's the code:

    public class LayoutSerializationBehavior : Behavior<DockSite>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.WindowActivated += AssociatedObject_LayoutUpdated;
            AssociatedObject.WindowDeactivated += AssociatedObject_LayoutUpdated;
            AssociatedObject.WindowsDragged += AssociatedObject_LayoutUpdated;
        }

        /// <summary>
        /// Anytime the ToolWindow layout changes, report it to the DependencyProperty.
        /// </summary>
        void AssociatedObject_LayoutUpdated(object sender, EventArgs e)
        {
            DockingWindow window = ((DockingWindowEventArgs)e).Window;
            if ( !(window is ToolWindow) ) return;

            DockSiteLayoutSerializer layoutSerializer = new DockSiteLayoutSerializer();
            layoutSerializer.SerializationBehavior = DockSiteSerializationBehavior.ToolWindowsOnly;
            layoutSerializer.DocumentWindowDeserializationBehavior = DockingWindowDeserializationBehavior.Discard;
            layoutSerializer.ToolWindowDeserializationBehavior = DockingWindowDeserializationBehavior.Discard;
            string layout = layoutSerializer.SaveToString(AssociatedObject);
            SetValue(LayoutXmlProperty, layout);
        }

        public string LayoutXml
        {
            //Return the most recently deserialized XML 
            get 
            {
                return (string)GetValue(LayoutXmlProperty); 
            }

            //When we set, update DependencyProperty AND restore the layout to the DockSite
            set
            {
                DockSiteLayoutSerializer layoutSerializer = new DockSiteLayoutSerializer();
                layoutSerializer.SerializationBehavior = DockSiteSerializationBehavior.ToolWindowsOnly;
                layoutSerializer.DocumentWindowDeserializationBehavior = DockingWindowDeserializationBehavior.Discard;
                layoutSerializer.ToolWindowDeserializationBehavior = DockingWindowDeserializationBehavior.Discard;
                layoutSerializer.LoadFromString(value, AssociatedObject);
                SetValue(LayoutXmlProperty, value);
            }
        }
 
        public static readonly DependencyProperty LayoutXmlProperty = DependencyProperty.Register("LayoutXml", typeof(string),
        typeof(LayoutSerializationBehavior), new FrameworkPropertyMetadata(null){BindsTwoWayByDefault = true});
    }

Then bind to it like:

            <i:Interaction.Behaviors>
                <local:LayoutSerializationBehavior LayoutXml="{Binding LayoutXml}"/>
            </i:Interaction.Behaviors>

And at last the ViewModel can serialize/deserialize without needing a reference to the DockSite itself.  The problem I have is getting it to update when needed, but not more.  The actual LayoutUpdated event fires constantly, so serializing/deserializing with such frequency is obviously out of the question.  However, I haven't been able to find a set of events that can fully handle this - for instance, when resizing a toolwindow.

So my question: is there any way to accomplish something like this in a way that doesn't result in excessive/constant serialization?  In reality I don't need it to deserialize everytime the layout changes, but this is the only approach I could think of to avoid needing a reference to the DockSite in my ViewModel...

Thanks in advance :)

Comments (11)

Posted 2 years ago by Justin Klein
Avatar

Addendum: I did think about a simpler solution of just doing something like:

<i:EventTrigger EventName="Loaded">
  <ei:CallMethodAction TargetObject="{Binding}" MethodName="RestoreLayout"/>
</i:EventTrigger>

 To call a method in the VM and deserialize there.  But that too has problems:

1) The Unloaded event is not called when you're shutting down the application, so there seems to be no good place to serialize/save the layout,

2) It doesn't permit saving/restoring on-demand (i.e. a 'restore default layout' command).  Without of course breaking MVVM & doing it from Code-Behind.

Posted 2 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Justin,

There are four main times you want to perform layout (de)serializations:

  1. DockSite.Loaded.
  2. User on-demand save.
  3. User on-demand load.
  4. Window.Closing.

Assuming you had a "LayoutXml" string property on your VM, you could:

  1. In the view's handling of a DockSite.Loaded event, access the VM LayoutXml and restore layout.
  2. Would need to watch for user-driven save requests (via button/menu click) in the view and save the layout to the VM LayoutXml property when those happen.
  3. Watch for updates to the VM LayoutXml property in the view and when detected, load the layout.  You would need to flag to make sure when you set it in scenarios like #2/#4, this logic doesn't execute.
  4. In the view's handling of a Window.Closing event, save the layout to the VM LayoutXml property.

With the above, you only do any layout serializations and deserialization when needed, and it doesn't require any DockSite reference in your VM.


Actipro Software Support

Posted 2 years ago by Justin Klein
Avatar

It seems that this approach would still require Code-Behind though, which is what I was really trying to avoid with an attached behavior.  If I did handle this feature with code-behind, it would literally be the first and only code-behind file in the entire software.  Is there no way to make serialization work while adhering to "purist" MVVM? :/

Posted 2 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Justin,

This is a tricky thing since various event sources trigger the four scenarios.  It's not like it all happens in one spot.  We're open to discussing any suggestions you have to have a more pure way of doing it, but for now, what I described is likely what you'd have to do.


Actipro Software Support

Posted 2 years ago by Justin Klein
Avatar

Well, at the very least, the situation of serializing/unseriailizing on start/shutdown could be handled if there were some DockSite "unload/cleanup/shutdown/etc"-type event that would consistently fire (since we already have a reliable one on startup, just not shutdown).

Or it could be handled if there were some event like WindowsLayoutChanged, which could be used as shown above.  An event that explicitly fires when i.e. a window opens, closes, changes focus, resizes, gets pinned, gets unpinned.  The param would include some info on the window itself, so you could ignore it if you only care about serializing toolwindow layout but not doc window layout.

I'm sure there are other optoins, and i'd be open to any - you guys are much better at WPF than me.  However, it just feels a bit of a shame to muck up CodeBehind in an otherwise 100% "clean" software just for this one feature :/

I'd be willing to limit it to only serializing on startup/shutdown, and not user-command, if it could be achieved in a pure MVVM way.

Posted 2 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Justin,

The problem is that we don't know when anything shuts down since while that is generally triggered by an ancestor Window closing, there could be other scenarios where the DockSite is embedded in other tab controls and those parent tab controls could be closed.  That scenario might trigger the DockSite.Unload event, but likely so would switching those parent tabs to other tabs and back.  On top of that, we've found the WPF Unload event in general to be fairly unreliable (it doesn't always fire when it should) in the past so we try to avoid using it.  I'm not really sure what to suggest since the app shutdown and user-driven scenarios are out of the range of what we ourselves can capture, as they change based on what our customers (you) implement.

[Modified 2 years ago]


Actipro Software Support

Posted 2 years ago by Justin Klein
Avatar

>>The problem is that we don't know when anything shuts down since while that is generally triggered by an ancestor Window closing, there could be other scenarios where the DockSite is embedded in other tab controls and those parent tab controls could be closed.

If the ancestor that triggered it were included in the EventArgs, then it could be up to the event handler to determine the appropriate action to take, no?  i.e. I'd handle that type of event, examine if the event were triggered by the actual main window closing - if so, serialize.  If not, act accordingly (aka do nothing).

Posted 2 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

We ourselves don't tap into any ancestor Window's closing events though since we don't need to know anything about the hierarchy above the root DockSite.  My point is that from our point of view, we don't know how you or other customers are using the DockSite and in which cases you mean for it to be shut down.  We can't assume that DockSite.Unload is always a shutdown situation, especially in nested DockSite scenarios where parent tabs might just be switching.

You could possibly look at the DockSite.Unload event on your own and if that is working for your needs, then just have an attached behavior that hits the DockSite.Loaded and Unloaded events and hook up your code that way.


Actipro Software Support

Posted 2 years ago by Justin Klein
Avatar

>>You could possibly look at the DockSite.Unload event on your own and if that is working for your needs

Doesn't work reliably.  From my 1st comment above: "The Unloaded event is not called when you're shutting down the application, so there seems to be no good place to serialize/save the layout"

Posted 2 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

That's why we generally (in apps) hook into Window.Closing, since that's a reliable way in most apps.  But you'd have to do that on your end since per above, we don't track any ancestor WPF Windows from the DockSite.  I suppose if you had an attached behavior, in that you could watch DockSite.Loaded and also walk the visual tree up to the ancestor Window to get it and attach to its Closing event. 


Actipro Software Support

Posted 2 years ago by Justin Klein
Avatar

Alright, I could give that a try.

More generally though, I suppose I might file this under "feature request: please consider making docksite serialization more MVVM-friendly?" :)  vNext did take some great strides to make the overall system more MVVM-adherent, but it looks like this aspect is one that could hopefully be similarly improved on.

Thanks!

The latest build of this product (v2019.1 build 0681) was released 1 month ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.