Fill My Prism Region, Please
posted by John with 15 comments
I’ve been working on fine tuning a Silverlight progress control for an article with Simple-Talk.com. Along the way I wanted to create a background that dims the entire page while the progress control is up and running. Easy stuff … just stick it in a Grid panel with HorizontalAlignment and VerticalAlignment set to Stretch. Works like a charm … except when it doesn’t
Enter regions in Prism … they are a very cool concept, kind of like a special zone in your app where you can place Views. In this case I have a Prism region I want to stick my Progress Control View in. Regions come out of the box in the form of either a ContentControl, TabControl, ItemsControl or Selector. None of these allowed me to stretch the control to the entire region.
According to the Prism docs, ContentControl is intended to store a single item. When I used this for my control, it put it in the upper left. Not good for what I needed. The Prism docs says the ItemsControl allows adding multiple views into a region (it is an items control after all). Not what I was looking for, but I tried it and of course it added the item to the top of a list (of 1) and centered my control horizontally, but not vertically. The Selector had similar results. And of course the TabControl region was not what I was looking for.
So … what to do … I decided to create a Region Adapter that is based off of the Grid. The Grid does exactly what I want in this case, it lets me put my progress control with a Rectangle background to fill and stretch to the entire region. The Prism docs explain that the 4 region types are all created using the RegionAdapter and that you can extend your own. My class does not need to do everything a Grid does, I just want a single column and a single row Grid panel. (Of course it can be extended to handle this if need be.)
The first step is to create a new class and derive from RegionAdapterBase<T>. In this case T is a Grid and I name my new class GridRegionAdapter. The code below is pretty straightforward, the important part is to implement the Adapt method. This is the method that fires when you add, remove, replace items from your region container. Since I just care about 1 row and 1 column, this is very straightforward.
using System.Windows;
using System.Windows.Controls;
using Microsoft.Practices.Composite.Presentation.Regions;
using Microsoft.Practices.Composite.Regions;
namespace MyUI.Infrastructure
{
public class GridRegionAdapter : RegionAdapterBase<Grid>
{
public GridRegionAdapter(IRegionBehaviorFactory behaviorFactory) :
base(behaviorFactory)
{
}
protected override void Adapt(IRegion region, Grid regionTarget)
{
region.Views.CollectionChanged += (s, e) =>
{
//Add
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
foreach (FrameworkElement element in e.NewItems)
regionTarget.Children.Add(element);
//Removal
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
foreach (FrameworkElement element in e.OldItems)
if (regionTarget.Children.Contains(element))
regionTarget.Children.Remove(element);
};
}
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
}
}
Now that the adapter exists, I can now use a Grid as a type of container in my Prism app and have my my control fill the entire screen.
The last step is to register the adapter in your Prism application. This can be done by adding the following code to the bootstrapper class.
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(Grid), Container.Resolve<GridRegionAdapter>());
return mappings;
}
It is quite simple to extend and create your own Region Adapters. However, I cannot stress enough that you need to carefully implement the Adapt method to handle everything your container needs to do. So just be careful to make sure it covers your bases.
Anonymous on said:
I ran into this same issue when i was working on Matchingo.. I ended up just hacking in hardcoded sizes in my modules due to tight time constraints.. ill have to give your grid solution a whirl next time.. thanks for sharing
Anonymous on said:
What an ugly hack
. It "feels" wrong because Grid is a Panel.
I haven’t done much Silverlight lately, but
the obvious solution in WPF to this would be to change the ContentControl’s template to use a Grid.
Something like this (not tested…)
<ControlTemplate x:Key="FilledContentControlTemplate"
TargetType="{x:Type ContentControl}">
<Grid>
<ContentPresenter />
</Grid>
</ControlTemplate>
I think the exact same thing should work for Silverlight.
Anonymous on said:
There’s an easier way…
Use an ItemsControl region adapter and set the ItemsPanel to be a Grid (by default it is a Stackpanel that does not expand in both directions)
JohnPapa on said:
Jean-Phillipe … Certainly can change the template for the ContentControl. Same results in the end … a control with a stretchable container intended to hold 1 element.
Tom … ItemsControl could also be tweaked, but in this case I wanted a single element only to be allowed at a time, and the ability to overlay. The Grid did this for me. Otherwise, yes your solution is also a fine choice.
The great thing about XAML is that we can override templates and often have several options.
Anonymous on said:
John,
I have tried following these steps, but the controls are still not stretching. Any chance that you have a sample that demonstrates this?
Anonymous on said:
Simpler solution:
<ItemsControl cal:RegionManager.RegionName="XXRegionNameXX">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Enjoy.
JohnPapa on said:
NVenhola …. yep, that’s another good solution
Anonymous on said:
Thank you all for your very helpful blog article as well as given comments (esp. the one of NVenhola).
Anonymous on said:
The differences between two solutions (making a new adapter for Panel-based control and using ItemPanelTemplate) are:
1. A new adapter for panel control is very useful in case you would like to implement some very specific panel like WrapPanel, a standard control of SL3
2. ItemPanelTemplate does not work with specific panels that are mentioned above.
Anonymous on said:
Jean-Philippe’s (#2) comment works with the following change:
<ControlTemplate x:Key="FilledContentControlTemplate" TargetType="ContentControl">
<Grid>
<ContentPresenter />
</Grid>
</ControlTemplate>
Ryan
Anonymous on said:
Hi all.
John, I copyed your code but did not work as I expected. As Scott asked before me, could you please provided a working demo?
My scenario is that I have 2 views registered on the same region (one for login and one for CRUD) and at any given time, only one must be active. This can easily be done with a ContentControl, but unfortunately, it does not fill the whole region.
I tried your approach and not only that the 2 views are not filling the region, but also they are displayed together (overlapped).
With NVenhola’s code, the views are filling the region, but they are still overlapped.
I seem to have hit another deadlock in my short time since I’m learning Prism.
Thanks.
Anonymous on said:
Hi can any one tell how to use login screen in prism based on user validation i have to load other region in prism , share some sample login page using prism .
Anonymous on said:
I tried your idea and toms so far i have to try Nvenhola and see wich one i like best. Thanks both ways work well for certain things.
kevin2themystic on said:
Thanks John! Great article!()); ());
I just wanted to let all MEFers out there that this line:
mappings.RegisterMapping(typeof(Grid), Container.Resolve
should be replaced with this line:
mappings.RegisterMapping(typeof(Grid), Container.GetExportedValue
kyle on said:
For those of you that only want one active view to be displayed at any time use this…
public class GridRegionAdapter : RegionAdapterBase
{
public GridRegionAdapter(IRegionBehaviorFactory behaviorFactory)
: base(behaviorFactory)
{
}
protected override void Adapt(IRegion region, Grid regionTarget)
{
region.ActiveViews.CollectionChanged +=
(s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Replace)
{
regionTarget.Children.Clear();
regionTarget.Children.Add((UIElement)region.ActiveViews.First());
}
};
region.Views.CollectionChanged +=
(s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add && region.ActiveViews.Count() == 0)
{
region.Activate(e.NewItems[0]);
}
};
}
protected override IRegion CreateRegion()
{
return new SingleActiveRegion();
}
}