The Microsoft TabControl is a popular control to use as a region in a WPF Prism application. Unfortunately, the Microsoft TabControl comes with some limitations, such as the inability to close tab items. If you want to close tab items in the Microsoft TabControl, you need to implement this functionality yourself. However, you can make this easy on yourself by using the
Infragistics xamTabControl
. The xamTabControl extends the Microsoft TabControl and adds a number of features that you would normally have to write yourself otherwise. For example; closing tab items is built into the control. Another great thing is that nothing syntactically really changes when you use the xamTabControl as a Prism Region. So switching out the TabControl for the Infragistics xamTabControl is no big deal. Everything is the same.
Let’s take a look at a view that uses the
xamTabControl
as a region and adds support for closing tab items.
<
Window.Resources
>
<
Style
TargetType
="{
x
:
Type
igWPF
:
TabItemEx}
">
<
Setter
Property
="Header"
Value
="{
Binding
DataContext
.Title}" />
</
Style
>
</
Window.Resources
>
<
Grid
>
<
Grid.RowDefinitions
>
<
RowDefinition
Height
="Auto" />
<
RowDefinition
Height
="*" />
</
Grid.RowDefinitions
>
<
StackPanel
Orientation
="Horizontal">
<
Button
Command
="{
Binding
NavigateCommand}
"
CommandParameter
="ViewA"
Content
="Navigate A"/>
<
Button
Command
="{
Binding
NavigateCommand}
"
CommandParameter
="ViewB"
Content
="Navigate B"/>
</
StackPanel
>
<
igWPF
:
XamTabControl
Grid.Row
="1"
prism
:
RegionManager.RegionName
="TabRegion" />
</
Grid
>
As you can see, this is a very simple view. It uses the
xamTabControl
as it’s region and it also has a couple of buttons on it that we will use to navigate to different views in the
xamTabControl
region. So when you run the application and click the buttons a couple of times, you will end up with something like this:
Okay, it’s nothing special right now. It looks just like it did if we were to use the Microsoft TabControl. Now let’s say we want to add support for closing tab items. Well, no problem! This is as easy as using a property. The TabItemCloseButtonVisibility property to be exact.
<
igWPF
:
XamTabControl
Grid.Row
="1"
prism
:
RegionManager.RegionName
="TabRegion"
TabItemCloseButtonVisibility
="Visible" />
Run the application and now we get those cool little close buttons automatically.
Pretty cool right? Heck yeah it’s cool! Run the app and start opening and closing tabs until your heart’s content. But…… We actually have a major issue here. We are using Prism. We have this thing called a Region. This region holds all of the views we add to it until we remove them from the region. Unfortunately, when you click on that pretty little close button, the view seems to be removed from the control, but in actuality it is still hanging around in our Region. This is because the close button has no idea that we are using Prism, or that we need to remove the view we just closed form the Region.
Solution
So how do we fix this? Simple… we need a custom behavior. This one right here:
public
class
TabItemRemoveBehavior
:
Behavior
<
XamTabControl
>
{
protected
override
void
OnAttached()
{
base
.OnAttached();
AssociatedObject.AddHandler(
TabItemEx
.ClosingEvent,
new
RoutedEventHandler
(TabItem_Closing));
AssociatedObject.AddHandler(
TabItemEx
.ClosedEvent,
new
RoutedEventHandler
(TabItem_Closed));
}
protected
override
void
OnDetaching()
{
base
.OnDetaching();
AssociatedObject.RemoveHandler(
TabItemEx
.ClosingEvent,
new
RoutedEventHandler
(TabItem_Closing));
AssociatedObject.RemoveHandler(
TabItemEx
.ClosedEvent,
new
RoutedEventHandler
(TabItem_Closed));
}
void
TabItem_Closing(
object
sender,
RoutedEventArgs
e)
{
IRegion
region =
RegionManager
.GetObservableRegion(AssociatedObject).Value;
if
(region ==
null
)
return
;
var
args = (
TabClosingEventArgs
)e;
args.Cancel = !CanRemoveItem(GetItemFromTabItem(args.OriginalSource), region);
}
void
TabItem_Closed(
object
sender,
RoutedEventArgs
e)
{
IRegion
region =
RegionManager
.GetObservableRegion(AssociatedObject).Value;
if
(region ==
null
)
return
;
RemoveItemFromRegion(GetItemFromTabItem(e.OriginalSource), region);
}
object
GetItemFromTabItem(
object
source)
{
var
tabItem = source
as
TabItemEx
;
if
(tabItem ==
null
)
return
null
;
return
tabItem.Content;
}
bool
CanRemoveItem(
object
item,
IRegion
region)
{
bool
canRemove =
true
;
var
context =
new
NavigationContext
(region.NavigationService,
null
);
var
confirmRequestItem = item
as
IConfirmNavigationRequest
;
if
(confirmRequestItem !=
null
)
{
confirmRequestItem.ConfirmNavigationRequest(context, result =>
{
canRemove = result;
});
}
FrameworkElement
frameworkElement = item
as
FrameworkElement
;
if
(frameworkElement !=
null
&& canRemove)
{
IConfirmNavigationRequest
confirmRequestDataContext = frameworkElement.DataContext
as
IConfirmNavigationRequest
;
if
(confirmRequestDataContext !=
null
)
{
confirmRequestDataContext.ConfirmNavigationRequest(context, result =>
{
canRemove = result;
});
}
}
return
canRemove;
}
void
RemoveItemFromRegion(
object
item,
IRegion
region)
{
var
context =
new
NavigationContext
(region.NavigationService,
null
);
InvokeOnNavigatedFrom(item, context);
region.Remove(item);
}
void
InvokeOnNavigatedFrom(
object
item,
NavigationContext
navigationContext)
{
var
navigationAwareItem = item
as
INavigationAware
;
if
(navigationAwareItem !=
null
)
{
navigationAwareItem.OnNavigatedFrom(navigationContext);
}
FrameworkElement
frameworkElement = item
as
FrameworkElement
;
if
(frameworkElement !=
null
)
{
INavigationAware
navigationAwareDataContext = frameworkElement.DataContext
as
INavigationAware
;
if
(navigationAwareDataContext !=
null
)
{
navigationAwareDataContext.OnNavigatedFrom(navigationContext);
}
}
}
}
Now simply apply the Behavior to our xamTabControl by using System.Windows.Interactivity. Like so:
<
igWPF
:
XamTabControl
Grid.Row
="1"
prism
:
RegionManager.RegionName
="TabRegion"
TabItemCloseButtonVisibility
="Visible" >
<
i
:
Interaction.Behaviors
>
<
core
:
TabItemRemoveBehavior
/>
</
i
:
Interaction.Behaviors
>
</
igWPF
:
XamTabControl
>
What this Behavior does is hooks into the TabItemEx.Closing and TabItemEx.Closed events so that we can execute our Prism logic during the tab item closing process. First, we need to respect whether we can even close the tab. This is done by checking the IConfirmNavigationRequest interface on the view we are trying to remove in the Closing event. If our View or ViewModel returns false during the ConfirmNavigationRequest method call, then the tab item cannot be closed. Otherwise, we will allow the tab item to be closed. For example; let’s say I had some rule that wouldn’t allow ViewB to be closed. I simply pass “false” in the continuationCallback within the ConfirmNavigationRequest method, and prompt the user letting them know why this tab couldn’t be closed.
This bring us to the TabItemEx.Closed event. Now that we know if we can or cannot close a tab, we simply get the view from the tab item, and then remove it from the region. But, before we remove it, we want to invoke the NavigatedFrom method on the INavigationAware interface so that we can respond to when a tab is closed from within our View or ViewModel to do any last minute clean up or state management.
That’s it! You have now added complete Prism Navigation support for closing tab items when using the
xamTabControl
as a Prism region. Hopefully you will find this useful, and maybe even use this in your WPF Prism applications. Be sure to check out the
source code
, and start playing with it. As always, feel free contact me on my
blog
, connect with me on Twitter (
@brianlagunas
), or leave a comment below for any questions or comments you may have.
Thanks for this, helped me understand behaviors better. One question though, in your AddHandler, you are adding a new RoutedEventHandler. In your RemoveHandler, you are removing a new RoutedEventHandler – since you are newing it, won’t that be a completely different reference than the one you registered initially? How will it be able to unsubscribe it?