The Prism region adapter I wrote for the xamDockManager has become much more popular than I would have ever thought. My last update to the xamDockManger Prism region adapter was back in March. That update included some refactoring and support for programmatic view activation and removal , and support for floating panes. Originally the xamDockManager Prism region adapter was designed with View first MVVM development in mind. Meaning that you would actually inject Views, in the form of a UI control, into the xamDockManager regions. Well, I have gotten a number of requests to support ViewModel first MVVM development, in which you inject ViewModels into the xamDockManager region and you control the creation of the Views with DataTemplates. I also had some bugs reported when dealing with nested regions within the xamDockManager. So lets take a look at the updated xamDockManager Prism region adapter.
The structure remains unchanged:
- TabGroupPaneRegionAdapter
- TabGroupPaneRegionActiveAwareBehavior
- IDockAware
TabGroupPaneRegionAdapter
{
/// <summary>
/// Used to determine what views were injected and ContentPanes were generated for
/// </summary>
private static readonly DependencyProperty IsGeneratedProperty = DependencyProperty.RegisterAttached(“IsGenerated”, typeof(bool), typeof(TabGroupPaneRegionAdapter), null);
/// <summary>
/// Used to track the region that a ContentPane belongs to so that we can access the region from within the ContentPane.Closed event handler
/// </summary>
private static readonly DependencyProperty RegionProperty = DependencyProperty.RegisterAttached(“Region”, typeof(IRegion), typeof(TabGroupPaneRegionAdapter), null);
public TabGroupPaneRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
: base(regionBehaviorFactory)
{
}
protected override void Adapt(IRegion region, TabGroupPane regionTarget)
{
if (regionTarget.ItemsSource != null)
throw new InvalidOperationException(“ItemsSource property is not empty. This control is being associated with a region, but the control is already bound to something else. If you did not explicitly set the control’s ItemSource property, this exception may be caused by a change in the value of the inherited RegionManager attached property.”);
SynchronizeItems(region, regionTarget);
region.Views.CollectionChanged += (Object sender, NotifyCollectionChangedEventArgs e) =>
{
OnViewsCollectionChanged(sender, e, region, regionTarget);
};
}
protected override void AttachBehaviors(IRegion region, TabGroupPane regionTarget)
{
base.AttachBehaviors(region, regionTarget);
if (!region.Behaviors.ContainsKey(TabGroupPaneRegionActiveAwareBehavior.BehaviorKey))
region.Behaviors.Add(TabGroupPaneRegionActiveAwareBehavior.BehaviorKey, new TabGroupPaneRegionActiveAwareBehavior { HostControl = regionTarget });
}
protected override IRegion CreateRegion()
{
return new SingleActiveRegion();
}
private void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, IRegion region, TabGroupPane regionTarget)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
//we want to add them behind any previous views that may have been manually declare in XAML or injected
int startIndex = e.NewStartingIndex;
foreach (object newItem in e.NewItems)
{
ContentPane contentPane = PrepareContainerForItem(newItem, region);
if (regionTarget.Items.Count != startIndex)
startIndex = 0;
//we must make sure we bring the TabGroupPane into view. If we don’t a System.StackOverflowException will occur in
//UIAutomationProvider.dll if trying to add a ContentPane to a TabGroupPane that is not in view.
//This is most common when using nested TabGroupPane regions. If you don’t need this, you can comment it out.
regionTarget.BringIntoView();
regionTarget.Items.Insert(startIndex, contentPane);
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (regionTarget.Items.Count == 0)
return;
IEnumerable<ContentPane> contentPanes = XamDockManager.GetDockManager(regionTarget).GetPanes(PaneNavigationOrder.VisibleOrder);
foreach (ContentPane contentPane in contentPanes)
{
if (e.OldItems.Contains(contentPane) || e.OldItems.Contains(contentPane.Content))
contentPane.ExecuteCommand(ContentPaneCommands.Close);
}
}
}
/// <summary>
/// Takes all the views that were declared in XAML manually and merges them with the region.
/// </summary>
private void SynchronizeItems(IRegion region, TabGroupPane regionTarget)
{
if (regionTarget.Items.Count > 0)
{
foreach (object item in regionTarget.Items)
{
PrepareContainerForItem(item, region);
region.Add(item);
}
}
}
/// <summary>
/// Prepares a view being injected as a ContentPane
/// </summary>
/// <param name=”item”>the view</param>
/// <returns>The injected view as a ContentPane</returns>
protected ContentPane PrepareContainerForItem(object item, IRegion region)
{
ContentPane container = item as ContentPane;
if (container == null)
{
container = new ContentPane();
container.Content = item; //the content is the item being injected
container.DataContext = ResolveDataContext(item); //make sure the dataContext is the same as the item. Most likely a ViewModel
container.SetValue(IsGeneratedProperty, true); //we generated this one
CreateDockAwareBindings(container);
}
container.SetValue(RegionProperty, region); //let’s keep track of which region the container belongs to
container.CloseAction = PaneCloseAction.RemovePane; //make it easy on ourselves and have the pane manage removing itself from the XamDockManager
container.Closed += Container_Closed;
return container;
}
void Container_Closed(object sender, PaneClosedEventArgs e)
{
ContentPane contentPane = sender as ContentPane;
if (contentPane != null)
{
contentPane.Closed -= Container_Closed; //no memory leaks
IRegion region = contentPane.GetValue(RegionProperty) as IRegion; //get the region associated with the ContentPane so that we can remove it.
if (region != null)
{
if (region.Views.Contains(contentPane)) //we are dealing with a ContentPane directly
region.Remove(contentPane);
var item = contentPane.Content; //this view was injected and set as the content of our ContentPane
if (item != null && region.Views.Contains(item))
region.Remove(item);
}
ClearContainerForItem(contentPane); //reduce memory leaks
}
}
/// <summary>
/// Checks to see if the item being injected implements the IDockAware interface and creates the necessary data bindings.
/// </summary>
/// <remarks>
/// First we will check if the ContentPane.Content property implements the IDockAware interface. Depending on the type of item being injected,
/// this may be a View or a ViewModel. If no IDockAware interface is found to create a binding, we next move to the ContentPane.DataContext property, which will
/// most likely be a ViewModel.
/// </remarks>
/// <param name=”container”></param>
void CreateDockAwareBindings(ContentPane contentPane)
{
Binding binding = new Binding(“Header”);
//let’s first check the item that was injected for IDockAware. This may be a View or ViewModel
var dockAwareContent = contentPane.Content as IDockAware;
if (dockAwareContent != null)
binding.Source = dockAwareContent;
//nothing was found on the item being injected, let’s check the DataContext
if (binding.Source == null)
{
//fall back to data context of the content pane, which is most likely a ViewModel
var dockAwareDataContext = contentPane.DataContext as IDockAware;
if (dockAwareDataContext != null)
binding.Source = dockAwareDataContext;
}
contentPane.SetBinding(ContentPane.HeaderProperty, binding);
}
/// <summary>
/// Sets the Content property of a generated ContentPane to null.
/// </summary>
/// <param name=”contentPane”>The ContentPane</param>
protected void ClearContainerForItem(ContentPane contentPane)
{
if ((bool)contentPane.GetValue(IsGeneratedProperty))
{
contentPane.ClearValue(ContentPane.HeaderProperty); //remove any bindings
contentPane.Content = null;
}
}
/// <summary>
/// Finds the DataContext of an item.
/// </summary>
/// <remarks>
/// If we are injecting a View, the result will be the Views DataContext. If we are injecting a ViewModel, the result will be the ViewModel.
/// </remarks>
/// <param name=”item”>The item</param>
/// <returns>A Views DataContext or a ViewModel</returns>
private object ResolveDataContext(object item)
{
FrameworkElement frameworkElement = item as FrameworkElement;
return frameworkElement == null ? item : frameworkElement.DataContext;
}
}
}
TabGroupPaneRegionActiveAwareBehavior
{
public const string BehaviorKey = “TabGroupPaneRegionActiveAwareBehavior”;
XamDockManager _parentDockManager;
TabGroupPane _hostControl;
public DependencyObject HostControl
{
get { return _hostControl; }
set { _hostControl = value as TabGroupPane; }
}
protected override void OnAttach()
{
_parentDockManager = XamDockManager.GetDockManager(_hostControl);
if (_parentDockManager != null)
_parentDockManager.ActivePaneChanged += DockManager_ActivePaneChanged;
Region.ActiveViews.CollectionChanged += ActiveViews_CollectionChanged;
}
void DockManager_ActivePaneChanged(object sender, RoutedPropertyChangedEventArgs<ContentPane> e)
{
if (e.OldValue != null)
{
var item = e.OldValue;
//are we dealing with a ContentPane directly
if (Region.Views.Contains(item) && Region.ActiveViews.Contains(item))
{
Region.Deactivate(item);
}
else
{
//now check to see if we have any views that were injected
var contentControl = item as ContentControl;
if (contentControl != null)
{
var injectedView = contentControl.Content;
if (Region.Views.Contains(injectedView) && Region.ActiveViews.Contains(injectedView))
Region.Deactivate(injectedView);
}
}
}
if (e.NewValue != null)
{
var item = e.NewValue;
//are we dealing with a ContentPane directly
if (Region.Views.Contains(item) && !this.Region.ActiveViews.Contains(item))
{
Region.Activate(item);
}
else
{
//now check to see if we have any views that were injected
var contentControl = item as ContentControl;
if (contentControl != null)
{
var injectedView = contentControl.Content;
if (Region.Views.Contains(injectedView) && !this.Region.ActiveViews.Contains(injectedView))
Region.Activate(injectedView);
}
}
}
}
void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
//are we dealing with a view
FrameworkElement frameworkElement = e.NewItems[0] as FrameworkElement;
if (frameworkElement != null)
{
ContentPane contentPane = frameworkElement as ContentPane;
if (contentPane == null)
contentPane = frameworkElement.Parent as ContentPane;
if (contentPane != null && !contentPane.IsActivePane)
contentPane.Activate();
}
else
{
//must be a viewmodel
object viewModel = e.NewItems[0];
var contentPane = GetContentPaneFromFromViewModel(viewModel);
if (contentPane != null)
contentPane.Activate();
}
}
}
ContentPane GetContentPaneFromFromViewModel(object viewModel)
{
var panes = XamDockManager.GetDockManager(_hostControl).GetPanes(PaneNavigationOrder.VisibleOrder);
foreach (ContentPane contentPane in panes)
{
if (contentPane.DataContext == viewModel)
return contentPane;
}
return null;
}
}
IDockAware
{
string Header { get; set; }
}
The Updated Region Adapter in Action
Nothing here has really changed from the previous posts. The main difference is now you can inject ViewModels and use DataTemplates to control your Views. Let’s take a look at the ShellViewModel.
{
IRegionManager _regionManager;
IUnityContainer _container;
private ObservableCollection<ViewAViewModel> _views;
public ObservableCollection<ViewAViewModel> Views
{
get { return _views; }
set
{
_views = value;
NotifyPropertyChanged(“Views”);
}
}
public DelegateCommand<object> ActivateCommand { get; set; }
public DelegateCommand<object> OpenCommand { get; set; }
public DelegateCommand<object> CloseCommand { get; set; }
public ShellViewModel(IUnityContainer container, IRegionManager regionManager)
{
_container = container;
_regionManager = regionManager;
ActivateCommand = new DelegateCommand<object>(Activate);
OpenCommand = new DelegateCommand<object>(Open, CanOpen);
CloseCommand = new DelegateCommand<object>(Close);
//generate some data
Views = new ObservableCollection<ViewAViewModel>();
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
Views.Add(_container.Resolve<ViewAViewModel>());
}
void Activate(object viewModel)
{
if (viewModel == null)
return;
IRegion region = _regionManager.Regions[KnownRegionNames.TabGroupPaneOne]; //get the region
if (region.Views.Contains(viewModel))
region.Activate(viewModel); //activate the viewModel
}
void Close(object viewModel)
{
if (viewModel == null)
return;
IRegion region = _regionManager.Regions[KnownRegionNames.TabGroupPaneOne]; //get the region
if (region.Views.Contains(viewModel))
region.Remove(viewModel); //remove the viewModel
}
void Open(object viewModel)
{
IRegion region = _regionManager.Regions[KnownRegionNames.TabGroupPaneOne]; //get the region
if (!region.Views.Contains(viewModel))
region.Add(viewModel); //add the viewModel
region.Activate(viewModel); //active the viewModel
}
bool CanOpen(object viewModel)
{
return viewModel != null;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion //INotifyPropertyChanged
}
As you can see, we are no longer referencing any type of View in the ViewModel. You are only acting against the ViewModels. This is a good thing, and I always suggest removing any type of View related objects out of your ViewModel. To render our View, we just create a DataTemplate that defines how the View should look for each instance of the inject ViewModel.
<DataTemplate DataType=”{x:Type moduleA:ViewAViewModel}“>
<StackPanel TextBlock.TextAlignment=”Center” TextBlock.FontSize=”24″ VerticalAlignment=”Center”>
<TextBlock Text=”{Binding Number}” />
<TextBlock Text=”{Binding IsActive, StringFormat=IsActive: {0}}” />
</StackPanel>
</DataTemplate>
</Window.Resources>
This DataTemplate is being applied implicitly to all instances of type ViewAViewModel. There is not a single View defined in the module. Now you can deliver a much more MVVM friendly application using the xamDockManager in using Prism.
Feel free to download the new and improved XamDockManager Prism region adapter with sample source code. I probably haven’t covered every possible use of this adapter, so if you find a scenario that should be support let me know. If you have any questions feel free to contact me through my blog, on twitter (@BrianLagunas), or leave a comment below.
Hello,
If I’ve multiple TabGroupPane opened (multiple screens) how can I choose on which one to create the next ContentPane.
I’m using _regionManager.RequestNavigate but the ContentPane is always created on the first TabGroupPane.
Thanks
In order to inject your views into different TabGroupPanes, those panes have to be prism regions. If you are dynamically creating these, you should used scoped regions, and use the region manager of the scoped region to navigate the views to the proper TabGroupPane.
If your TabeGroupPanes are static, you can simply give those different region names and navigate to those regions instead.
What I’d like to have is the following :
When one ContentPane (viewmodel) does a request to open a new one, the last would be added into the current TabGroupPane.
How can I retrieve the current scoped RegionManager ?
I would recommend using custom region behavior that will create the scoped region for you and attach the region manager with an attached property. This way you can access it directly from the object being injected..
Hi Brian,
In order to display different views in TabGroupPanes, i inject ViewModels and use DataTemplates to control the view, i have tested using the solution provided here and i made some changes. i have changed the DataTemplates to use the custom user control i created.
I used the memory profiler to monitor the memory usage, i found that when i closed the ContentPane, the user control and the associated view model instances do not destroy/ dispose properly.
In the codes, when we closed one of the content pane, the associated ContentPane.Content will be set to null and the ContentPane will be removed from the Region. But this does not destroy/dispose the view models and user control properly.
How can i ensure that the view model is disposed correctly when it’s closed? Please advice.
Which profiler are you using? You need to find out what is holding onto your ViewModel objects. Your memory profiler should be able to help you find this.
Thanks for the reply.
I am using ANTS Memory Profiler 7. Actually the TabItemAutomationPeer & ContentPane are holding the UserControl and ViewModel objects. Is there a way to dispose the ContentPane when it’s closed?
I have posted the same problem in http://www.infragistics.com/community/forums/p/77267/436367.aspx#436367. I have also uploaded the source codes which i used for memory testing. You could find this attachment in the same web site.
Looking forward to hearing from you. Thank you.
Hi Brian,
when i add many contentpanes to the xamDockManager (XMD). How i can place this contentpane to the right of the XDM?
When there are many contentPanes, the next contentPane will be placed on the left (default behaviour??)…
Thank you.
The panes should be added to the right by default, as I am incrementing the index to insert the pane by one each time. If you want to change this behavior, you just need to modify the TabGroupPaneRegionAdapter at line 66. Simply calculate your own insertion index before calling the Insert method.
I test with your sample. When adding the 11th contentpane, it’s be placed on the right before ‘default 1’ content pane (the insertion index = 10). My screen have a resolution of 1280*1024 and this sample is running in maximized mode. Can you check this behavior?
In minimized mode, the 8th contentpane will be placed on the left….
Many thanks
I think I understand what you mean now. Keep in mind, my adapter showed an example of modifying the insert index to be on the right of the previous tab, which is not the default behavior. The default behavior is to open new tabs to the left of the previous tab just like Visual Studio. To keep the default behavior, simply remove all the “startIndex” code, and at line 79 just use regionTarget.Items.Insert(0, contentPane);. Now all tabs will be added to the left and in view, while pushing all the other tabs to the right.
Hi Brian,
This is just awesome. We are planning to use this in a trading application that we will be writing for a bank. Could you please help us out with the layout persistence.
Thanks a ton.
Hi Brian,
How could I add a tooltip on the tab header ?
If I use contentPane.SetBinding(ContentPane.ToolTipProperty, binding); it adds a tooltip in the body of the view not on the header.
Thanks in advance
You have to set the tooltip on the header of the content pane. So, thinking off the top of my head, you would need to create an implicit style that would provide a new HeaderTemplate with the binding for the Tooltip added.
I just got a question from our support team asking the same problem, so I figured I would provide the Tooltip solution here too. This is Assuming that there is a “HeaderToolTip” property on the bound ViewModel.
var headerTemplate = new DataTemplate();
FrameworkElementFactory textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetBinding(TextBlock.TextProperty, new Binding(“”));
textBlock.SetBinding(TextBlock.ToolTipProperty, new Binding(“DataContext.DataContext.HeaderToolTip”) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ContentControl), 1) });
headerTemplate.VisualTree = textBlock;
//if inside a DocumentContentHost
contentPane.TabHeaderTemplate = headerTemplate;
Thanks
I modified TabGroupPaneRegionAdapter like the following
void CreateDockAwareBindings(object item, ContentPane contentPane)
{
contentPane.HeaderTemplate = contentPane.FindResource(“HeaderTemplate”) as DataTemplate;
Binding bindingHeader = new Binding(“Header”);
Binding bindingAllowClose = new Binding(“AllowClose”);
Binding bindingImage = new Binding(“Image”);
Binding bindingFullHeader = new Binding(“FullHeader”);
//let’s first check the view that was injected for IDockAware
var dockAwareContent = contentPane.Content as IDockAware;
if (dockAwareContent != null)
{
bindingHeader.Source = dockAwareContent;
bindingAllowClose.Source = dockAwareContent;
bindingImage.Source = dockAwareContent;
bindingFullHeader.Source = dockAwareContent;
}
//fall back to data context of the content pane.
var dockAwareDataContext = contentPane.DataContext as IDockAware;
if (dockAwareDataContext != null)
{
bindingHeader.Source = dockAwareDataContext;
bindingAllowClose.Source = dockAwareDataContext;
bindingImage.Source = dockAwareDataContext;
bindingFullHeader.Source = dockAwareDataContext;
}
contentPane.SetBinding(ContentPane.HeaderProperty, bindingHeader);
contentPane.SetBinding(ContentPane.AllowCloseProperty, bindingAllowClose);
contentPane.SetBinding(ContentPane.ImageProperty, bindingImage);
}
public interface IDockAware
{
string Header { get; set; }
string FullHeader { get; set; }
bool AllowClose { get; set; }
ImageSource Image { get; }
}
Unfortunately, I has no effect on the header. I must be missing a detail.
Well, without having a running sample of your app, I can only start guessing. I would start by making sure your source implements INotifyPropertyChanged.
Hi,
With XamDockManager when I try to drag a tab it take around 5 seconds before the tab gets detached.
Once the tab is detached the performance is ok.
Do you know where that time is spend and how I can improve ?
Thank in advance.
Sorry for the late reply, I haven’t been getting notifications of new comments for some reason. This is really hard to say. I haven’t experienced this issue before, so it could possibly be your application code. I suggest reaching out to support and see if they can help. Try the xamDockManager forums first http://www.infragistics.com/community/forums/220.aspx