AvalonDock is a fancy, open source tab control for WPF which i wanted to use in one of my clients projects because it supports save/restore layout. Unfortunately, binding the avalondock control to your view model isn’t as easy as it is with the standard wpf tab control. Hence i googled a bit and found this article in the AvalonDock documentation and a blog entry which deals with the problem but both didn’t really solve the problem. What i needed was a notification mechanism for the gui when the conductor changes (or closes) its ActiveItem and vice versa, i.e. a two-way binding.
So, how can we implement that. Well, my idea was to make the conductor aware of the view and implement an interface in the view which allows me to change/close the active item in the AvalonDock control and to register to the events when the user activates/closes a tab in the control. The interface i came up with is this:
public interface IAvalonDockView<T>
{
DockingManager DockingManager { get; }
void Show(T item);
void Close(T item);
event EventHandler<ContentActivatedEventArgs<T>> ContentActivated;
event EventHandler<ContentClosingEventArgs<T>> ContentClosing;
}
public class ContentActivatedEventArgs<T> : EventArgs
{
public T Item;
}
public class ContentClosingEventArgs<T> : EventArgs
{
public T Item;
public bool Cancel;
}
I think it is pretty straightforward. One thing to note though is the cancel flag in the ContentClosingEventArgs which allows the conductor to cancel the closing of a tab item (f.e. when the viewmodels TryClose() method returns false). I choosed to not implement the interface directly in my view but in a seperate implementor class and the view basically acts as an adapter for the implementor.
public class AvalonDockViewImpl<T> : IAvalonDockView<T>
{
private readonly ILog _log = LogManager.GetLog(typeof (AvalonDockViewImpl<T>));
/// <summary>
/// Returns the name for the dockable content of the item. This name must be unique for each item if you want to use the save/restore layout feature!
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public Func<T, string> ResolveDockableContentName { get; set; }
/// <summary>
/// Returns the title for the dockable content of the item. This name must not be unique !
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public Func<T, string> ResolveDockableContentTitle { get; set; }
public AvalonDockViewImpl(DockingManager dockingManager)
{
DockingManager = dockingManager;
dockingManager.ActiveDocumentChanged += (s, e) =>
{
var dc = dockingManager.ActiveDocument as DockableContent;
OnDcActivated(dc);
};
ResolveDockableContentName = item => string.Format("Item{0}", item.GetHashCode().ToString());
ResolveDockableContentTitle = item => item is IHaveDisplayName
? (item as IHaveDisplayName).DisplayName
: typeof (T).Name;
}
#region IAvalonDockView<T> Members
public DockingManager DockingManager { get; private set; }
public void Show(T item)
{
_log.Info("Showing {0}", ResolveDockableContentName(item));
object view = LocateViewFor(item);
DockableContent dc = GetDockableContentFromView(view);
if (dc == null)
{
_log.Info("No dockable content found for {0}. Creating a new one", ResolveDockableContentName(item));
dc = CreateDockableContent(item);
_log.Info("Created dockable content: {0}", dc.Name);
dc.ShowAsDocument(DockingManager);
}
else
{
_log.Info("Dockable content {0} found for {1}", dc.Name, ResolveDockableContentName(item));
dc.Activate();
if (dc.State == DockableContentState.Hidden)
{
dc.Show();
}
if (dc.State == DockableContentState.DockableWindow)
{
((FloatingDockablePane) (dc.Parent)).FloatingWindow.Focus();
((FloatingDockablePane) (dc.Parent)).FloatingWindow.Topmost = true;
}
}
}
public void Close(T item)
{
_log.Info("Showing {0}", item is IHaveDisplayName ? (item as IHaveDisplayName).DisplayName : typeof (T).Name);
object view = LocateViewFor(item);
DockableContent dc = GetDockableContentFromView(view);
if (dc != null)
{
dc.Close(true);
}
}
public event EventHandler<ContentActivatedEventArgs<T>> ContentActivated;
public event EventHandler<ContentClosingEventArgs<T>> ContentClosing;
#endregion
private DockableContent GetDockableContentFromView(object view)
{
return DockingManager.DockableContents
.SingleOrDefault(x => x.Content == view);
}
private DockableContent CreateDockableContent(T item)
{
object view = LocateViewFor(item);
var dockableContent = new DockableContent
{
Content = view,
Name = ResolveDockableContentName(item),
Title = ResolveDockableContentTitle(item),
HideOnClose = false
};
dockableContent.Closing += (s, e) =>
{
var dc = (s as DockableContent);
_log.Info("Attempting to close dc {0}", dc.Name);
T vm = LocateViewModelFor(dc.Content);
bool cancel = false;
if (ContentClosing != null)
{
var closingEventArgs = new ContentClosingEventArgs<T>
{
Item = vm
};
ContentClosing(this, closingEventArgs);
cancel = closingEventArgs.Cancel;
}
if (cancel)
{
_log.Info("Closing of dc {0} canceled", dc.Name);
}
e.Cancel |= cancel;
};
dockableContent.Closed += (s, e) =>
{
var dc = (s as DockableContent);
dc.Content = null;
};
dockableContent.IsActiveContentChanged += (s, e) =>
{
var dc = (s as DockableContent);
OnDcActivated(dc);
};
return dockableContent;
}
private void OnDcActivated(DockableContent dc)
{
T vm = LocateViewModelFor(dc.Content);
if (dc.IsActiveContent)
{
_log.Info("Changed active dc to {0}", dc.Name);
if (ContentActivated != null)
{
ContentActivated(this, new ContentActivatedEventArgs<T> {Item = vm});
}
}
}
public static object LocateViewFor(T item)
{
UIElement view = ViewLocator.LocateForModel(item, null, null);
ViewModelBinder.Bind(item, view, null);
return view;
}
public static T LocateViewModelFor(object view)
{
object viewModel = ViewModelLocator.LocateForView(view);
return (T) viewModel;
}
}
The Show() method checks if there already exists a dockable content for the view and if so makes it the active tabitem or window. If there doesn’t exist a dockable content, one is created and activated. We also register to various events of the dockable content, notably to the closing event to cancel the closing of the dockable content if the view model permits it. The two Func<T,string> which resolve the title (what is shown in the tab header) and name (used for saving/restoring layouts) for the dockable content can be changed easily in the view. Note that the name of the dockable content must be unique if you want to use the save/restore layout feature and that you can bind to the title property if you need to. We will take a look at the layout feature in the second part. The Close() method looks for the dockable content which host the view and closes it. Easy stuff.
For the conductor, i decided to subclass the existsing Conductor<T>.Collection.OneActive since that is exactly the behavior i wanted. So i only had to override some methods, namely:
public class AvalonConductor<T> : Conductor<T>.Collection.OneActive
{
private IAvalonDockView<T> View { get; set; }
protected override void ChangeActiveItem(T newItem, bool closePrevious)
{
T previous = ActiveItem;
base.ChangeActiveItem(newItem, closePrevious);
if (closePrevious && !Equals(previous, ActiveItem))
{
View.Close(previous);
}
}
public override void ActivateItem(T item)
{
bool updateView = !Equals(item, ActiveItem);
base.ActivateItem(item);
if (updateView) View.Show(ActiveItem);
}
protected override T DetermineNextItemToActivate(int lastIndex)
{
// this will be handled by the view
return default(T);
}
protected override void OnViewLoaded(object view)
{
base.OnViewLoaded(view);
var avalonDockView = view as IAvalonDockView<T>;
if (avalonDockView != null)
{
View = avalonDockView;
View.ContentActivated += (s, e) => ActivateItem(e.Item);
View.ContentClosing += (s, e) =>
{
DeactivateItem(e.Item, true);
e.Cancel = Items.Contains(e.Item);
};
}
}
}
When the view is loaded we register to its events and call the appropiate methods when the event is fired. And when the conductor activates/closes an item, we call the the corresponding method on the view. One thing to note is that the view actually determines the next active item if the current is closed so we just return null.
That’s it for the first part. I also created a small demo project which can be downloaded here