Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

AvalonDock [2.0] Tutorial Part 5 - Load/Save Layout with De-Referenced DockingManager

0.00/5 (No votes)
25 Feb 2014 1  
Save/Load AvolonDock layouts without DockingManager references.

Introduction    

This article is split into 2 major sections. The first section shows you how to load/save AvalonDock layouts on application start-up and shut-down. The second section builds on the presented approach and implements two more commands to persist and re-load layouts during the run-time of the program. Each section has its own associated download.

You can also find a similar sample application in my own repository that I started in the mean time (if you feel like contributing) https://github.com/Dirkster99/AvalonDock.

Loading/Saving AvalonDock Layouts at Start-up/Shut-Down

Please download and inspect the code in the Version_05_Edi.zip file to follow the discussion presented in this section.

I recently helped a friend getting started with his AvalonDock developments and it occurred to me that many people have come to value the MVVM pattern, but not as many people actually know why it is important. That is, everyone seems to be aware of MVVM and how it de-couples layers but not many people seem to know why de-coupling is actually necessary in WPF. 

We know about separation of concerns and that kind of architectural mumbo-jambo but what if I just do not care and use WPF as I used WinForms? Just store a reference in the viewmodel and call an appropriate method or set a property when the application logic sees fit:  

public DockingManager ADManager{ get; set; }

// Implement some processing logic like saving and loading layouts
// through a reference of the view element in the viewmodel
public void UpdateLayout()
{
  this.ADManager.UpdateLayout();
  this.ADManager. ...
}

Mixing the WinForms style of programming with MVVM (as sketched above) is a no go. This style of programming can lead to an application that is unstable, not deterministic, and difficult to debug. This instability can typically occur since WPF applications implement more than 1 thread [2] and communicating between these threads can be a pain if you avoid using things like dependency properties, commanding via binding, (routed) events, routed commands, and all these other little things that set a WPF application apart from the rest of the computer science world.  

This article shows how you can use AvalonDock without referencing the DockingManager instance in your viewmodel or anywhere below it. This is a valuable exercise, especially for beginners, since removing hard references to parts of the view within the viewmodel ensures that threading problems resulting in unstable applications, are a thing of the past.  

The sample processing task used in this section of the article is to load and save the document layout at application start-up/shut-down. This valuable sample exercise is extended in the next major section futher below.

Preparation 

This article is based on the solution of the last article that was published in step 4 of this tutorial [1]. You can either download the solution from the previous article and add each code piece as we step through the article or download the complete solution from this article to verify required code changes.

You should be aware of attached behaviours [3] to understand this article. Just look at the samples I've documented in the other article and come back when you get the hang of it. I am not going into the depth of attached behaviors here since I rather focus on its application with AvalonDock.

Using the Code

The heart of my solution is an attached behavior [3] on the DockingManager instance in the MainWindow.xaml code. This behavior reacts whenever the DockingManager instance fires the Load or Unload event. 

So, we can add the attached behavior in its associated namespace and folder, say Edi.View.Behavior (see sample code), call the new attached behavior class AvalonDockLayoutSerializer and paste the code from the sample solution into the file.

 

Next we can adjust the MainWindow.xaml to make use of the new attached behavior class. Add a reference to the above namespace:

xmlns:AVBehav="clr-namespace:Edi.View.Behavior"

...and use that reference in the XAML code like so:

<avalonDock:DockingManager
  AnchorablesSource="{Binding Tools}" 
  DocumentsSource="{Binding Files}"
  ActiveContent="{Binding ActiveDocument, Mode=TwoWay, Converter={StaticResource ActiveDocumentConverter}}"
  
  AVBehav:AvalonDockLayoutSerializer.LoadLayoutCommand="{Binding ADLayout.LoadLayoutCommand}"
  AVBehav:AvalonDockLayoutSerializer.SaveLayoutCommand="{Binding ADLayout.SaveLayoutCommand}"
  
  Grid.Row="2">

Note that we are no longer using the x:Name="dockManager" property in the above code. This code is used in virtually every AvalonDock (sample) code and it may lead to frustrating times if you are new to WPF  So, removing this x:Name property may require you to remove corresponding code references to make this work. 

How precisely does the above code work? The attached behavior in AvalonDockLayoutSerializer can bind and execute a command for loading and saving of layout files. The LoadLayoutCommand command is executed when the DockingManager fires the Load event. The Load event is a standard WPF event indicating the instantiation of a view. The Unload event is also a standard WPF event indicating that a view is about to be destroyed. This event results in executing the SaveLayoutCommand.

Both LoadLayoutCommand and SaveLayoutCommand are bound to a viewmodel property called ADLayout (of type AvalonDockLayoutViewModel) in the Workspace class. The AvalonDockLayoutViewModel class exposes the properties for the save/load layout commands.

So, to complete this solution, we need to add the code for the AvalonDockLayoutViewModel class in the Edi.ViewModel namespace. And add the property in the Workspace class:

private AvalonDockLayoutViewModel mAVLayout = null;

/// <summary>
/// Expose command to load/save AvalonDock layout on application startup and shut-down.
/// </summary>
public AvalonDockLayoutViewModel ADLayout
{
  get
  {
    if (this.mAVLayout == null)
      this.mAVLayout = new AvalonDockLayoutViewModel();

    return this.mAVLayout;
  }
}

There are also some utility properties like Workspace.LayoutFileName, DirAppData, and Company but these are simple string properties that are only there to avoid hard coded and redundant string definitions. You can locate these properties if you browse through the sample solution.

I know I could use the blend interactivity dlls to replace the attached behavior in this article but I would like to make this exercise as simple and valuable as I can. So, research blend interactivity on your own if you think you know what this article is about.

You can also download my editor (https://edi.codeplex.com/) if you want to see the behavior working in a more complex scenario. 

Load/Save Layouts via Commands 

Summarizing the previous sections: We are using an attached behavior to listen to an event, convert it into a command, and execute it via command binding. These scenarios are simple to solve once the attached behavior pattern is well understood [3] and applied correctly.

  1. View.Event
  2. Attached Behavior
  3. Execute Command bound to viewmodel
  4. Execute corresponding viewmodel method

There are also use cases that require save and load of AvalonDock layouts during the life time of an application. These use cases may call for a more involved implementation where:  

  1. The viewmodel initiates the function
  2. The view implements the corresponding processing and returns a result
  3. The viewmodel completes the processing (for example, by storing the result in a database).

I name the above scenario "round trip " because it involves the viewmodel starting some processing, requires the view to finish it, and the viewmodel to implement final steps of the processing. It is, therefore, more complex to implement this correctly. I did not have a good idea for a solution when I started to write this article but someone here at CodeProject (see forum below) came along and brought the Event Aggregator [4] pattern to my attention. And it turns out to be a good solution for exactly this type of problem as you can see in the Version_05_LoadSaveCommand.zip download.  

This solution (see Version_05_LoadSaveCommand.zip) is based on MVVM Light and is a little bit more involved. You will see here that I dropped the AvalonDock and AvalonEdit projects and replaced them by a Nuget reference. There is also a Nuget references to MVVM Light since we use this framework to implement the Event Aggregator pattern. 

The code is class-vise very similar to the initial implementation discussed in the first part of this article. But there are 2 new buttons in the user interface, which I will discuss next.

The Save Layout  button is bound to the ADLayout.SaveWorkspaceLayoutToStringCommand property in the ApplicationViewModel class. The ADLayout.SaveWorkspaceLayoutToStringCommand property is implemented in the AvalonDockLayoutViewModel class same as in the previous solution. This command is bound to the     SaveWorkspaceLayout_Executed() method with the following publisher code:

Messenger.Default.Send(new NotificationMessageAction<string>(
                       Notifications.GetWorkspaceLayout,
                       (result) =>
                       {
                         this.current_layout = result;
                       }));

This line  creates a new NotificationMessageAction<string> object and sends it off to whoever subscripped to this type of message. The result of this message is a string, which is, when the result returns, set to the member string current_layout. But I am jumping ahead here. This does not make much sense until we have looked at the subscribers side of things - so lets look into MainWindow.xaml.cs to understand both sides of that story.

The constructor of the MainWindow class in MainWindow.xaml.cs  has this line:

Messenger.Default.Register<NotificationMessageAction<string>>
  (this, notication_message_action_recieved);

It is a subscription to notifications of the NotificationMessageAction<string> class and tells MVVM Light to execute the MainWindow.notication_message_action_recieved method whenever such a notification is send by a publisher. The method is then used to save the layout from the DockingManager and return it in the final execute statement as string parameter:

string xmlLayoutString = string.Empty;

using (StringWriter fs = new StringWriter())
{
  XmlLayoutSerializer xmlLayout = new XmlLayoutSerializer(this.dockManager);
  xmlLayout.Serialize(fs);
  xmlLayoutString = fs.ToString();
}

message.Execute(xmlLayoutString);

...now we have seen both sides of the story and so we can understand why current_layout is set with the Xml layout string of the DockingManager class. 

You may notice that we have the dockManager reference in MainWindow.xaml back in place (we removed it in the above approach). This is also OK for an MVVM compliant design since the field is private (via x:FieldModifier private) and it is never passed anywhere outside of the MainWindow class.

<avalonDock:DockingManager AnchorablesSource="{Binding Tools}"
 x:Name="dockManager" x:FieldModifier="private"
 DocumentsSource="{Binding Files}"
 ActiveContent="{Binding ActiveDocument, Mode=TwoWay, Converter={StaticResource ActiveDocumentConverter}}"
 IsEnabled="{Binding IsBusy, Converter={StaticResource BooleanNotConverter}, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
 AVBehav:AvalonDockLayoutSerializer.LoadLayoutCommand="{Binding ADLayout.LoadLayoutCommand}"
 AVBehav:AvalonDockLayoutSerializer.SaveLayoutCommand="{Binding ADLayout.SaveLayoutCommand}"

 Grid.Row="2">

The above reference is fine if it conforms to the requirements that it is private and is not passed to other classes outside of the view. But this is difficult and can be overseen (as I have done in an earlier version of the sample code). So, the best case MVVM implementation is still a view that does not need an x:Name property. But it is more then OK to have it carefully implemented when we have to face reality and such constrains limited time, effort, and so forth.

The sequence diagram below gives us a birds eye view and leads us to see the irony of that story. The irony is that a user starts a function in the MainWindow which sends itself a message to save the layout of the DockingManager that is contained in the MainWindow(!)

 

This particular case could of course be implemented easier. But imagine that the DockingManager may be contained in a UserControl that is either placed into the MainWindow or into a different window. Or think about placing the Save Layout button into a completely different part of the GUI - an extra dialog or a seperate window. The above solution would work for all these cases and more, which is important as you discover advanced WPF features.

The Load Layout button is enabled as soon as the current_layout string in the AvalonDockLayoutViewModel class is set to a value. You can change the arrangements of the displayed tool windows and click Load Layout to confirm that it really loads the previously saved layout.  The function of that method is also implemented through the Event Aggregator discussed above. You should now be able to understand and verify its function. Just post a question if you still see open ends.

Conclusions

This article presents 3 solutions that should help you saving and loading the layout of the DockingManager class in your own application. The 2nd part, is based on MVVM Light, and would not be here unless someone here at CodeProject (see forum below) had contributed his idea. Thanks a lot for that. 

Other frameworks, such as Caliburn.Micro or PRISM also support the Even Aggregation pattern. I developed the PRISM solution by re-engineering it from the MVVM Light solution. Feel free to send me your sample code if you feel like implementing an equivalent sample with Caliburn.Micro. Otherwise, I am off to the next step of the tutorial step and look forward to be in touch. 

In any case, I am as always, looking forward to your feedback.

References 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here