Cancel a Tab closing event if a view is dirty

Nov 10, 2008 at 9:12 PM
Hi all, I have a Composite WPF design question.  If a TabItem (associated with a Region) is closed by a user, I need any views inside the tab to display a warning message to the user if the view is dirty ("You have unsaved changes.  Are you sure you want to close this tab?" yes or no).  Here's my setup:

I am attaching a Region to a WPF TabControl.  UserControls with their own Regions are then added to the TabControls' region.  The TabControl's TabItems have close buttons which close that TabItem.  I made my own custom TabControlRegionAdapter, which is very similar to the SelectorRegionAdapter and I've confirmed that when my tabs are closed by the user, the region that goes with that TabItem is also destroyed (I'm basically using the SelectorRegionSyncBehavior from the CAL source).

I can have an event handler for TabItem.Closing in my TabControlRegionAdapter, and in there I can set e.Cancel = true if I want to cancel the event.  However, before actually closing the tab, I want to check to make sure that none of the views added to the Regions defined in the UserControl inside my TabItem are dirty before just closing it.  If they are dirty, then they need to be able to display a modal dialog letting the user choose whether or not to actually close the TabItem.  Depending on their choice, I will either cancel the Closing event or let it proceed.

Any thoughts on a good pattern for how to do this?  I will have many views (or perhaps PresentationModels) that will need to check their is dirty state.  I was thinking of creating an interface, say IAmDirtyable, that my views or models could implement.  Then in the TabItem.Closing event handler I could somehow loop through all the views add to the regions inside the UserControl view contained in the TabItem--if they implement IAmDirtyable would call the IsDirty() method on each of them and display a message or proceed with the event if appropriate.  IAmDirtyable might also have to have a Save() method.

Anybody out there who has already solved this problem satisfactorily?  Am I on the right track here or way off in the weeds?

Here's my code for TabControlRegionAdapter if it's helpful.  I'm actually using the Infragistics XamTabControl with TabItemEx both of which inherit from TabControl and TabItem respectively. 

public class TabControlRegionAdapter : RegionAdapterBase<TabControl>
    {
        /// <summary>
        /// Adapt
        /// </summary>
        /// <param name="region"></param>
        /// <param name="regionTarget"></param>
        protected override void Adapt(IRegion region, TabControl regionTarget)
        {
            //If control has child items, move them to the region and then bind control to region.
            if (regionTarget.Items.Count > 0)
            {
                throw new InvalidOperationException("message here");
            }


            if (regionTarget.Items.Count > 0)
            {
                //Control must be empty before setting ItemsSource
                foreach (object childItem in regionTarget.Items)
                {
                    region.Add(childItem);
                }
                regionTarget.Items.Clear();
            }
            regionTarget.ItemsSource = region.Views;
            
            region.Views.CollectionChanged += delegate(Object sender, NotifyCollectionChangedEventArgs e)
            {
                OnViewsCollectionChanged(sender, e, region, regionTarget);
            };
        }

        private static void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, TabControl regionTarget)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                //An item was added to the region.  We now have to add it to the TabItem.
                foreach (object item in e.NewItems)
                {
                    DependencyObject view = item as DependencyObject;
                    if (view != null)
                    {
                       TabItemEx tabItem = (regionTarget as TabControl).ItemContainerGenerator.ContainerFromItem(view) as TabItemEx;
                       //Set a header on the tab if the property has been attached
                       SetInfoOnTab(view, tabItem);
                       tabItem.Closing += new EventHandler<global::Infragistics.Windows.Controls.Events.TabClosingEventArgs>(tabItem_Closing);
                    }
                }
            }

            
        }

        static void tabItem_Closing(object sender, global::Infragistics.Windows.Controls.Events.TabClosingEventArgs e)
        {
            //TODO: need to give the user control the ability to cancel this if its dirty
            //so we probably need to fire a PRISM tabClosing event to notify all the user controls.
            e.Cancel = false;
        }

        private static void SetInfoOnTab(DependencyObject view, TabItemEx tabItem)
        {
            IRegionInfo tabInfo = view.GetRegionInfo();
            if (null != tabInfo)
            {
                tabItem.Header = tabInfo.Header;
            }
        }

        /// <summary>
        /// CreateRegion
        /// </summary>
        /// <returns></returns>
        protected override IRegion CreateRegion()
        {
            return new Region();
        }

        /// <summary>
        /// Attach new behaviors.
        /// </summary>
        /// <param name="region">The region being used.</param>
        /// <param name="regionTarget">The object to adapt.</param>
        
        protected override void AttachBehaviors(IRegion region, TabControl regionTarget)
        {
            base.AttachBehaviors(region, regionTarget);
            //The behavior uses weak references while listening to events to prevent memory leaks
            //when destroying the region but not the control or viceversa.
            SelectorRegionSyncBehavior syncBehavior = new SelectorRegionSyncBehavior(regionTarget, region);
            syncBehavior.Attach();
            
        }





Apr 21, 2009 at 1:57 AM
adajos - I am curious if you have a working sample?  I am trying to do something similar for Silverlight.  If you don't mind I would love to see your sample and see what we could do to port it over to Silverlight.  Thanks,  Matt
Apr 21, 2009 at 8:17 PM
mkduffi:

Yes, I ended up with an implementation that I am fairly happy with. 

Here's the what I did.

1.  Basically I created an interface something like this:

public

 

interface ISaveableBeforeClosing
{
   bool IsDirty();
}

 

In UserControls in which I care about this "Isdirty" kind of thing I implement this interface.  That way the logic needed to determine whether or not a view is dirty is left to the view itself because it will be very custom for each view. In retrospective I wish I had named the interface IAmClosing and had the method name be bool CancelClose() because it does nothing specific to save as that is left to the view, and we've had several views implemented in which they never display a message about saving, but autosave and only then close the tab.   So semantically it could be better but it works out fine.

2.  In my TabControlRegionAdapter I subscribe to the tab closing event every time I add a new tab like so:

private

 

static void OnViewsCollectionChanged(NotifyCollectionChangedEventArgs e, XamTabControl regionTarget)
{
   if (e.Action == NotifyCollectionChangedAction.Add)
   {
      foreach (object item in e.NewItems)
      {
         DependencyObject view = item as DependencyObject;
         if (view != null)
        {
            TabItemEx tabItem = (TabItemEx)regionTarget.ItemContainerGenerator.ContainerFromItem(view);
            tabItem.Closing += tabItem_Closing;
        }
      }
    }
}

 


3.  put an event handler for the TabItemEx closing event in my TabControlRegionAdapter

private

 

static void tabItem_Closing(object sender, global::Infragistics.Windows.Controls.Events.TabClosingEventArgs e)
{
   TabItemEx tab = (TabItemEx)e.Source;
   ISaveableBeforeClosing saveableView = tab.Content as ISaveableBeforeClosing;
    if (saveableView != null)
    {
         e.Cancel = saveableView.IsDirty();
     }

 


This event handler cancels the tab closing event if one of the views says to do so.

A couple of notes about this implementation:

1.  Our TabControlAdapter actually became a XamTabControl adaptor (the Infragistics TabControl which extends the WPF TabControl).  XamTabControl contains the Infragistics' TabItemEx (which extends WPF TabItem), and TabItemEx has the cancellable TabClosing event built into it, but this would not work out of the box with the normal WPF TabItem.  However it would be an easy matter to extend TabItem and give it a cancellable event to wire up to.
2.  One non-obvious implication of this is that if your TabItem.Content contains a view which itself has regions that in turn contain other views that implement ISaveableBeforeClosing, then you need to make your "root" view implement ISaveableBeforeClosing and recurse through all the regions it contains, and go through all the views in those regions to see if they implement ISaveableBeforeClosing and then calling IsDirty() on them.  If you don't want to do that, you could just have your "root" view implement the interface and then call out to a Controller, an event, or something to check on the state of other views. 

When our entire Window closes we fire of a TabClosing event for each tab in the TabControl, and when that happens each tab makes sure its views give it permission to close as well, which essentially means that a given view can prevent the entire shell from closing down if need me. 

This method has worked well for us.  That said, I bet somebody else has figured out a better way to do it than this.

Good luck mkduffi.





Jun 25, 2010 at 10:54 AM
Edited Jun 25, 2010 at 10:55 AM

Hi,

I get a problem with this in the OnViewsCollectionChanged event handler. When I hit the line:

                        TabItemEx tabItem =
                           regionTarget.ItemContainerGenerator.ContainerFromItem(view) as TabItemEx;

tabItem is returned as null. The reason being because the regionTarget.Items.Count = 0! The CollectionChanged event seems to be firing before the view has been added to the XamTabControl. Has anyone else seen this?

 

Any help would be much appreciated!

Thanks

Gary

Jul 12, 2010 at 6:56 AM

i understand this is an old post, but just a note here:

you could use something like iterating

_regionManager.Regions["MyRegion"].ActiveViews

and than cast each view to your interface

 

v5.5.41 desktopdefault.cs_regionMan