3
votes

When navigating between Views/ViewModels using RequestNavigate (i.e. programmatically), the IConfirmNavigationRequest methods on the appropriate ViewModels are called as expected. However, if you switch views in a TabControl region by clicking on the tab, it does not call those methods.

Is this the expected and accepted behaviour? Would I be able to implement a prism behavior to make this work?

Any advice would be appreciated.

UPDATE

I've decided to explain the problem more thoroughly based on Viktor's feedback. I want to prevent navigation if the user has unsaved edits on the screen. Switching tabs IMHO is just another way to navigate. I expect the Prism implementation to be consistent: navigating programmatically or otherwise should have the same behaviour.

If I were to create an ItemsControl with buttons that when clicked navigates by using RequestNavigate (to effectively switch tabs) it would work, but that isn't the point of the question.

2
Does each tab in the tab control have its own region?Big Daddy

2 Answers

2
votes

I suppose I can see your point, and I understand why you would like it to call the RequestNavigate method.

To answer your question, yes this is by design and it is not supposed to call RequestNavigate while switching tabs. However, you can modify this behavior to do what you want. Prism is open source. You should have the source code, you can add the project to your project and easily step through the code for the following:

TabControlRegionAdapter - Adapts the region to the tab control

public class TabControlRegionAdapter : RegionAdapterBase<TabControl>
    {
        /// <summary>
        /// <see cref="Style"/> to set to the created <see cref="TabItem"/>.
        /// </summary>
        public static readonly DependencyProperty ItemContainerStyleProperty =
            DependencyProperty.RegisterAttached("ItemContainerStyle", typeof(Style), typeof(TabControlRegionAdapter), null);

        /// <summary>
        /// Initializes a new instance of the <see cref="TabControlRegionAdapter"/> class.
        /// </summary>
        /// <param name="regionBehaviorFactory">The factory used to create the region behaviors to attach to the created regions.</param>
        public TabControlRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
            : base(regionBehaviorFactory)
        {
        }

        /// <summary>
        /// Gets the <see cref="ItemContainerStyleProperty"/> property value.
        /// </summary>
        /// <param name="target">Target object of the attached property.</param>
        /// <returns>Value of the <see cref="ItemContainerStyleProperty"/> property.</returns>
        public static Style GetItemContainerStyle(DependencyObject target)
        {
            if (target == null) throw new ArgumentNullException("target");
            return (Style)target.GetValue(ItemContainerStyleProperty);
        }

        /// <summary>
        /// Sets the <see cref="ItemContainerStyleProperty"/> property value.
        /// </summary>
        /// <param name="target">Target object of the attached property.</param>
        /// <param name="value">Value to be set on the <see cref="ItemContainerStyleProperty"/> property.</param>
        public static void SetItemContainerStyle(DependencyObject target, Style value)
        {
            if (target == null) throw new ArgumentNullException("target");
            target.SetValue(ItemContainerStyleProperty, value);
        }

        /// <summary>
        /// Adapts a <see cref="TabControl"/> to an <see cref="IRegion"/>.
        /// </summary>
        /// <param name="region">The new region being used.</param>
        /// <param name="regionTarget">The object to adapt.</param>
        protected override void Adapt(IRegion region, TabControl regionTarget)
        {
            if (regionTarget == null) throw new ArgumentNullException("regionTarget");
            bool itemsSourceIsSet = regionTarget.ItemsSource != null;

            if (itemsSourceIsSet)
            {
                throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException);
            }
        }

        /// <summary>
        /// Attach new behaviors.
        /// </summary>
        /// <param name="region">The region being used.</param>
        /// <param name="regionTarget">The object to adapt.</param>
        /// <remarks>
        /// This class attaches the base behaviors and also keeps the <see cref="TabControl.SelectedItem"/> 
        /// and the <see cref="IRegion.ActiveViews"/> in sync.
        /// </remarks>
        protected override void AttachBehaviors(IRegion region, TabControl regionTarget)
        {
            if (region == null) throw new ArgumentNullException("region");
            base.AttachBehaviors(region, regionTarget);
            if (!region.Behaviors.ContainsKey(TabControlRegionSyncBehavior.BehaviorKey))
            {
                region.Behaviors.Add(TabControlRegionSyncBehavior.BehaviorKey, new TabControlRegionSyncBehavior { HostControl = regionTarget });
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="Region"/>.
        /// </summary>
        /// <returns>A new instance of <see cref="Region"/>.</returns>
        protected override IRegion CreateRegion()
        {
            return new SingleActiveRegion();
        }
    }

And also, TabControlRegionSyncBehavior. This is the one which you could call RequestNavigate.

 public class TabControlRegionSyncBehavior : RegionBehavior, IHostAwareRegionBehavior
    {
        ///<summary>
        /// The behavior key for this region sync behavior.
        ///</summary>
        public const string BehaviorKey = "TabControlRegionSyncBehavior";

        private static readonly DependencyProperty IsGeneratedProperty =
            DependencyProperty.RegisterAttached("IsGenerated", typeof(bool), typeof(TabControlRegionSyncBehavior), null);

        private TabControl hostControl;

        /// <summary>
        /// Gets or sets the <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
        /// </summary>
        /// <value>A <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
        /// This is usually a <see cref="FrameworkElement"/> that is part of the tree.</value>
        public DependencyObject HostControl
        {
            get
            {
                return this.hostControl;
            }

            set
            {
                TabControl newValue = value as TabControl;
                if (newValue == null)
                {
                    throw new InvalidOperationException(Resources.HostControlMustBeATabControl);
                }

                if (IsAttached)
                {
                    throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach);
                }

                this.hostControl = newValue;
            }
        }

        /// <summary>
        /// Override this method to perform the logic after the behavior has been attached.
        /// </summary>
        protected override void OnAttach()
        {
            if (this.hostControl == null)
            {
                throw new InvalidOperationException(Resources.HostControlCannotBeNull);
            }

            this.SynchronizeItems();

            this.hostControl.SelectionChanged += this.OnSelectionChanged;
            this.Region.ActiveViews.CollectionChanged += this.OnActiveViewsChanged;
            this.Region.Views.CollectionChanged += this.OnViewsChanged;
        }

        /// <summary>
        /// Gets the item contained in the <see cref="TabItem"/>.
        /// </summary>
        /// <param name="tabItem">The container item.</param>
        /// <returns>The item contained in the <paramref name="tabItem"/> if it was generated automatically by the behavior; otherwise <paramref name="tabItem"/>.</returns>
        protected virtual object GetContainedItem(TabItem tabItem)
        {
            if (tabItem == null) throw new ArgumentNullException("tabItem");
            if ((bool)tabItem.GetValue(IsGeneratedProperty))
            {
                return tabItem.Content;
            }

            return tabItem;
        }

        /// <summary>
        /// Override to change how TabItem's are prepared for items.
        /// </summary>
        /// <param name="item">The item to wrap in a TabItem</param>
        /// <param name="parent">The parent <see cref="DependencyObject"/></param>
        /// <returns>A tab item that wraps the supplied <paramref name="item"/></returns>
        protected virtual TabItem PrepareContainerForItem(object item, DependencyObject parent)
        {
            TabItem container = item as TabItem;
            if (container == null)
            {
                object dataContext = GetDataContext(item);
                container = new TabItem();
                container.Content = item;
                container.Style = TabControlRegionAdapter.GetItemContainerStyle(parent);
                container.DataContext = dataContext; // To run with SL 2
                container.Header = dataContext; // To run with SL 3                  
                container.SetValue(IsGeneratedProperty, true);
            }

            return container;
        }

        /// <summary>
        /// Undoes the effects of the <see cref="PrepareContainerForItem"/> method.
        /// </summary>
        /// <param name="tabItem">The container element for the item.</param>
        protected virtual void ClearContainerForItem(TabItem tabItem)
        {
            if (tabItem == null) throw new ArgumentNullException("tabItem");
            if ((bool)tabItem.GetValue(IsGeneratedProperty))
            {
                tabItem.Content = null;
            }
        }

        /// <summary>
        /// Creates or identifies the element that is used to display the given item.
        /// </summary>
        /// <param name="item">The item to get the container for.</param>
        /// <param name="itemCollection">The parent's <see cref="ItemCollection"/>.</param>
        /// <returns>The element that is used to display the given item.</returns>
        protected virtual TabItem GetContainerForItem(object item, ItemCollection itemCollection)
        {
            if (itemCollection == null) throw new ArgumentNullException("itemCollection");
            TabItem container = item as TabItem;
            if (container != null && ((bool)container.GetValue(IsGeneratedProperty)) == false)
            {
                return container;
            }

            foreach (TabItem tabItem in itemCollection)
            {
                if ((bool)tabItem.GetValue(IsGeneratedProperty))
                {
                    if (tabItem.Content == item)
                    {
                        return tabItem;
                    }
                }
            }


            return null;
        }

        /// <summary>
        /// Return the appropriate data context.  If the item is a FrameworkElement it cannot be a data context in Silverlight, so we use its data context.
        /// Otherwise, we just us the item as the data context.
        /// </summary>
        private static object GetDataContext(object item)
        {
            FrameworkElement frameworkElement = item as FrameworkElement;
            return frameworkElement == null ? item : frameworkElement.DataContext;
        }

        private void SynchronizeItems()
        {
            List<object> existingItems = new List<object>();
            if (this.hostControl.Items.Count > 0)
            {
                // Control must be empty before "Binding" to a region
                foreach (object childItem in this.hostControl.Items)
                {
                    existingItems.Add(childItem);
                }
            }

            foreach (object view in this.Region.Views)
            {
                TabItem tabItem = this.PrepareContainerForItem(view, this.hostControl);
                this.hostControl.Items.Add(tabItem);
            }

            foreach (object existingItem in existingItems)
            {
                this.Region.Add(existingItem);
            }
        }

        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // e.OriginalSource == null, that's why we use sender.
            if (this.hostControl == sender)
            {
                foreach (TabItem tabItem in e.RemovedItems)
                {
                    object item = this.GetContainedItem(tabItem);

                    // check if the view is in both Views and ActiveViews collections (there may be out of sync)
                    if (this.Region.Views.Contains(item) && this.Region.ActiveViews.Contains(item))
                    {
                        this.Region.Deactivate(item);
                    }
                }

                foreach (TabItem tabItem in e.AddedItems)
                {
                    object item = this.GetContainedItem(tabItem);
                    if (!this.Region.ActiveViews.Contains(item))
                    {
                        this.Region.Activate(item);
                    }
                }
            }
        }

        private void OnActiveViewsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                this.hostControl.SelectedItem = this.GetContainerForItem(e.NewItems[0], this.hostControl.Items);
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove
                && this.hostControl.SelectedItem != null
                && e.OldItems.Contains(this.GetContainedItem((TabItem)this.hostControl.SelectedItem)))
            {
                this.hostControl.SelectedItem = null;
            }
        }

        private void OnViewsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                int startingIndex = e.NewStartingIndex;
                foreach (object newItem in e.NewItems)
                {
                    TabItem tabItem = this.PrepareContainerForItem(newItem, this.hostControl);
                    this.hostControl.Items.Insert(startingIndex, tabItem);
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (object oldItem in e.OldItems)
                {
                    TabItem tabItem = this.GetContainerForItem(oldItem, this.hostControl.Items);
                    this.hostControl.Items.Remove(tabItem);
                    this.ClearContainerForItem(tabItem);
                }
            }
        }
    }

Of course, you'll have to figure out where to call RequestNavigate, such that you can actually cancel the TabSelectionChanging. Unfortunately, this event doesn't exist in WPF. I would resort to the trick recommended by Josh Smith How to Prevent a TabItem from changing

1
votes

What I understood from your question is that you expect that switching tabs calls IConfirmNavigationRequest. Method from this interface is called when you navigating from view/viewModel implementing this interface.

But, what you experiencing when you switch tabs in TabControl is not Navigation request. All views in TabControl already handled Navigation operation and all views are already in TabControl(Your Region). So what you do when you switch tabs? You only Activating view within your region. Previously active view gets deactivated.

I really don't know what are you trying to accomplish. I cannot imagine why whould I prevent somebody from switching tabs. But you could try that by using IActiveAware interface. You can get the idea from this blog

EDIT

  1. Implement OnDeactivate to ask user whether or not he wants to save changes before deactivating view

  2. Implement OnActivate to call RequestNavigate to already existing View. U can read about Navigating to Existing Views in Prism documentation.

  3. Disable all other tabItems and enable them again after saving changes(bad approach)

I am really not an expert, but I don't think you have more options left