WPF Prism - 使用 IConfirmNavigationRequest 阻止标签页切换无效。

3
当使用RequestNavigate(即以编程方式)在视图/视图模型之间导航时,适当的ViewModels上的IConfirmNavigationRequest方法按预期调用。但是,如果通过单击选项卡在TabControl区域中切换视图,则不会调用这些方法。
这是预期和接受的行为吗?我能否实现棱镜行为使其工作?
任何建议都将不胜感激。
更新
根据Viktor的反馈,我决定更详细地解释问题。如果用户在屏幕上有未保存的编辑内容,我想要防止导航。在我看来,切换选项卡只是另一种导航方式。我希望Prism实现保持一致:以编程方式或其他方式导航应具有相同的行为。
如果我创建一个ItemsControl,并通过使用RequestNavigate的按钮进行导航(以有效地切换选项卡),则它将起作用,但这不是问题的重点。

选项卡控件中的每个选项卡都有自己的区域吗? - Big Daddy
2个回答

2

我理解你的观点,也明白你为什么想要调用RequestNavigate方法。

回答你的问题,是的,这是按设计要求的,不应该在切换选项卡时调用RequestNavigate。但是,你可以修改此行为以实现你的需求。Prism是开源的。你应该有源代码,可以将项目添加到你的项目中,并轻松地跟踪以下代码:

TabControlRegionAdapter-将区域适配到选项卡控件中

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();
        }
    }

同时,TabControlRegionSyncBehavior是您可以调用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);
                }
            }
        }
    }

当然,您需要找到在哪里调用RequestNavigate,以便您实际上可以取消TabSelectionChanging。不幸的是,WPF中不存在此事件。我建议采用Josh Smith在其博客文章"How to Prevent a TabItem from changing"中推荐的技巧。请参考此链接

1
从您的问题中我理解到,您希望切换选项卡时调用IConfirmNavigationRequest方法。当您从实现此接口的视图/视图模型导航时,将调用此接口的方法。
但是,当您在TabControl中切换选项卡时,所经历的不是导航请求。TabControl中的所有视图已经处理了导航操作,并且所有视图都已经在TabControl(您的区域)中。因此,当您切换选项卡时,您只是激活您区域内的视图。以前处于活动状态的视图将被停用。
我真的不知道您想要实现什么。我无法想象为什么我要阻止某人切换选项卡。但是,您可以尝试使用IActiveAware接口来实现这一点。您可以从这个blog中获取灵感。 编辑
  1. 实现OnDeactivate方法,在停用视图之前询问用户是否要保存更改。

  2. 实现OnActivate方法,调用RequestNavigate方法到已存在的视图。您可以在Prism 文档中了解导航到现有视图的相关信息。

  3. 禁用所有其他选项卡并在保存更改后再启用它们(不是最佳方法)。

我真的不是专家,但我认为你没有更多的选择了。


谢谢您的回答。我已经更新了我的问题,以包含更多关于用例的细节。我确实查看了IActiveAware,但它只提供激活的单向通知。因此,您无法使用该接口更改活动视图或防止其更改。 - Andre Luus
如果你想阻止用户激活其他的选项卡项,你必须将所有选项卡项的属性IsEnabled设置为false!我认为这并不理想。 - Kapitán Mlíko
@AndreLuus,使用按钮可以获得不同的行为,您可以在命令实现中调用RequestNavigate方法,从而可以利用INavigationAware和IConfirmNavigationRequest。因为通过从TabControl选择不同的TabItem,您只是在区域内更改当前活动视图,所以无法这样做。请查看博客文章中IsActive属性的实现。您可以实现OnDeactivate方法来询问用户是否要保存更改。 - Kapitán Mlíko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接