Summary
AvalonDock is the most popular WPF open source docking library. Sofa wraps it, adds features and allows an easy integration of AvalonDock in many different architectures such as Prism.
In order to gain benefit from docking system and multi-instance features, an existing application must be converted into a Component that can be hosted in a docking Container.
This is fast and easy with Sofa: This article is a step by step description of this conversion.
Glossary of this Text
The Sofa.Container.SofaContainer
and Sofa.Commons.SofaComponents
are the main classes of Sofa
.
The SofaContainer
class is inserted in a WPF Window or a UserControl
. The application managing this Window or this UserControl
is named the Container in this documentation.
The SofaComponent
class embeds a user's application. This application is named the Component in this documentation.
Example: In the following examples, we reuse the CSWPFMasterDetailBinding
application. We transform it into a library and it becomes a Component. It will be run in the SofaBasiContainer
application which is a Container.
1. From Application To Component
The application is made of 2 projects:
1st project: Container: SofaBasicContainer
A Container has about nothing to do else ... containing. The SofaBasiContainer
Container we use can be found in many examples and is reused with a small modification: Only adjust the MEF importing declaration:
- The MEF
[ImportMany]
key in BaseWindow.xaml.cs is set to Sofa.Examples. SofaComponization.SofaBasicContainer_ViewContract
.
2nd project: Component: CSWPFMasterDetailBinding
In this step, an application is transformed into a Component and is prepared for registration in the previous SofaBasicContainer
Container.
The application is CSWPFMasterDetailBinding
. It is coming from the All-In-One Code Framework (WPF) project (http://1code.codeplex.com).
- Transform the Application into a Library:
- Project properties:
- Output Type is changed from Windows Application to Class Library.
- The library of this project must be reached by the
SofaBasicContainer
in order to use MEF: The build output path must be set to ..\SofaBasicContainer\bin\Debug\AddIns\
- The target framework is set to .NET 4
- App.xaml and App.xaml.cs are deleted. In some cases, the existing code must be moved to the Component's
Usercontrol
of the Container but there is none here.
- MainWindow.xaml: The
<Window x : Class ="CSWPFMasterDetailBinding.MainWindow"
is changed into<UserControl x :
Class... Related adjustments must be done to some tags (resources, triggers), properties (Title, events...) and partial class definition in the xaml and xaml.cs files.
- In order to become a Component, a library must only be able to register in a Container. This is done using MEF. The
[Export]
declarations and metadata are added in MainWindow.xaml.cs:
- A reference to
System.ComponentModel.Composition.Codeplex
DLL must be added in order to use MEF. Sofa
libraries are also referenced.
- The MEF
[Export]
declarations are always the same and can be copied from any example. At this step, the only important data to edit are:
- The
[Export]
key to adjust to the previous "Sofa.Examples. SofaComponization.SofaBasicContainer_ViewContract
" key used in the [ImportMany]
declaration of the Container.
- The
ProductName
(i.e. SofaComponent id
) set to "CSWPFMasterDetailBinding
" and the Label
.
That's it. You can run the SofaBasicContainer
Container and find the CSWPFMasterDetailBinding
Component in its menu. Load one or many, dock, undock, resize the windows...
Current result: The original application now runs in a Container and is a multi-instance application.
But its GUI has not changed...
2. From Simple Component to sub-Components
The goal of this step is to split the previous Component into 2 Components, each of them holding one of the lists of the previous Component.
The 2 Components can be hosted by the SofaBasicContainer
but they are always working together and it is better to host them in a new Container, this Container being also a Component hosted in the SofaBasicContainer
Container.
The " 1. Original Application To SofaComponent" folder is duplicated and renamed into "2. Simple SofaComponent to sub-SofaComponents".
1st project: Container: SofaBasicContainer
The SofaBasicContainer
project is a standard Container and does not need any modification.
2nd project: Component & Container: CSWPFMasterDetailBinding
- The original Component is duplicated.
- MainWindow.xaml class is duplicated. Files as well as classes are renamed: One is renamed "
CustomerListComponent
" and the other "OrderListComponent
".
- Only the
Customer
list (listViewCustomers
) is kept in the CustomerListComponent
and the Order
list (listViewOrders
) in OrderListComponent
. The <UserControl.Resources>
tag is related to CustomerList
and is deleted from the OrderList
.
- The
[Export]
keys are adjusted: The new target of the MEF exports is the container that will be created in the next step.
Sofa.Examples.SofaComponization
. SofaBasicContainer_ViewContract
is renamed to Sofa.Examples.SofaComponization
. MainComponent_ViewContract
ProductName
are renamed from CSWPFMasterD
etailBinding
to CustomerList
and OrderList
and Labels
are edited.
- New Component-Container
As we want the 2 Components are opened in a unique window, we need a new Container. This Container will also be a Component hosted in the initial SofaBasicContainer
in place of the previous MainWindow
Component.
This Container-Component is a new class named MainComponent
. It is a mixture of the SofaBasicContainer
(for Container code) and the previous MainWindow
class (for Component code):
Component
- The only code of a Component is the MEF
[Export]
declaration. The key is set to the same value as in the [ImportMany]
declaration of the SofaBasicContainer
.
Container
- The current project hosts a Container : References to
Sofa
libraries must be added.
- The class implements the
IBaseContainer
interface and must have the public SofaContainer SofaContainer
(used) and public SofaMenu SofaMenu
(not used) declarations.
- The class will import the 2 previous "
CustomerListComponent
" and "OrderListComponent
" Components. Its [ImportMany]
declaration is set to the same value as the Component's [Export]
keys. It implements the IPartImportsSatisfiedNotification
interface in order the OnImportsSatisfied
method is triggered. The Compose
method of the SofaBasicContainer
must be run once and is not present in this sub-Container.
- The "sub-Components" of this Component-Container cannot be opened before the Container is fully initialized. The only valid time is when handling the "
PostOpen
" SofaCommonEvent
in the SofaCommonEventHandler
method. The sofaContainer.OpenComponent("CustomerList")
and sofaContainer.OpenComponent("OrderList")
methods are used. Component's names are hardcoded that is not elegant but not definitive (see 3. Binding the 2 lists).
You can run the application and the 2 sub-Components will open in the MainComponent
, itself opening in the SofaBasicContainer
.
But the OrderListComponent
has no source for its binding and does not show the orders of the selected customer.
- Bind the 2 lists:
Everything is done in the code of the PostOpen
event handling of the MainComponent
: The 2 components are reached using the Sofacontainer SofaComponentList
and the listViewCustomers
of the CustomerListComponent
is used as DataContext
for the OrderListComponent
.
The application now works correctly.
3. About Perspectives ...
1st project: Container: SofaBasicContainer
This project implements a full perspective management scenario: Main features are load/save the last used perspective when opening/closing the Container and a menu allowing perspectives to be created, loaded and deleted.
Steps to implement this are:
- Resources: The
PerspectiveManager
helper and its associated PerspectiveName
are imported and namespaces adjusted.
- Actions triggers: The previous
OpenComponent
call that loaded the CSWPFMasterDetailBinding
Component in the BaseWindow
constructor is deleted. It is replaced by a perspective management: The Window_Loaded
and Window_Closed
events are handled and respectively load and save the last used perspective.
- Menus: Two menus are added; the first one allows basic functions such as
Create
/Delete
perspectives as the second shows the list of existing perspectives and allows swapping from one to another. Related C# code is added in the BaseWindow
to handle GUI events and forward them to the PerspectiveHelper
class.
2nd project: Container & Component: CSWPFMasterDetailBinding
The two Components currently open as in a WPF Tab Control. This presentation is relevant for independent windows such as the MasterComponent
instances but not when an action on one windows modifies the other one.
This project uses perspectives to load the 2 components side by side:
- Resources: As for the
SofaBasicContainer
, the PerspectiveManager
helper is imported.
- Actions: The
perspectiveHelper.LoadPerspective("MainComponent
")
method is added in the method handling the SofaCommon PostOpen
event and not surprisingly the perspectiveHelper.SaveCurrentPerspective()
in the " Closed
" event.
- The application is then run: The first time the
LoadPerspective
method is used, the perspective does not exist and this has no effect. Then the CSWPFMasterDetailBinding
Component is loaded from the menu, the user organizes them as desired and the perspective is saved when closing the application by the Closed
event handling. This creates the perspective that becomes a part of the application. The two sofaContainer.OpenComponent
methods of the PostOpen
event handling can then be deleted as the Components will automatically be opened with the perspective.
History
- 2nd September, 2011: Initial version