TabControlRegionAdapter: how to confirm tab switching

Topics: Prism v4 - Silverlight 4
Nov 21, 2010 at 10:39 AM

Hi,

I am trying to make use of the AnimatedTabControl from the RI of Prism 4 and kind of stuck on how to prevent the user from switching the tabs when there are changes yet to be comitted.

Basically I would like to confirm if they want to loose the change and navigate to the other view of stay in the same view when a tab header is clicked.

I also tried to make changes to the AnimatedTabControl code itself, even that dose not work.

 

        protected override void OnSelectionChanged(SelectionChangedEventArgs args)
        {
            if (args.RemovedItems.Count > 0)
            {
                bool allowNavigation = true;
                var switching = ((TabItem)args.RemovedItems[0]).Content as IViewSwitching;
                if (switching != null) { allowNavigation = switching.CanNavigateOut(); }
                if (allowNavigation)
                {
                    this.RestoreBufferedTabItemContent();

                    // Put the "old" view in a buffer so we can still show it to perform the starting animation with it
                    this.previousSelectedTabItem = (TabItem)args.RemovedItems[0];
                    this.previousSelectedTabItemContent = (FrameworkElement)this.previousSelectedTabItem.Content;
                    this.previousSelectedTabItem.Content = null;
                    this.CurrentView.Visibility = Visibility.Collapsed;
                    this.BufferView.Content = this.previousSelectedTabItemContent;
                    this.StartingTransition.Begin();
                }
                else
                {
                    this.SelectedItem = args.RemovedItems[0];
                }
            }
        }

 but that throws an exception as it goes to recursion. Would appriciate any help to solve this.

Thanks,Kiran

Developer
Nov 23, 2010 at 5:04 PM

Hi Kiran,

You might find these blog posts from Brian Noyes useful for achieving your purpose:

Even though they are targeting slightly different scenarios, you could follow a similar approach as the ones described in the blog posts for your situation.

You might also find the RegionActiveAwareBehavior behavior useful for your scenario.

I hope you find this helpful.

Guido Leandro Maliandi
http://blogs.southworks.net/gmaliandi

Nov 24, 2010 at 3:51 PM

Hi,

Thanks for your reply, but I don't really understand how I can prevent the the Prism shell from switching the Tab as I see no events I can hook in to. In the example link you provided, there is a OnWindowClosing where we can cancle the action, but with the RegionAdaptor on the TabControl there is no such option.

Is there an event I can hook in to, I have already tried to change the OnSelectionChanged but dose not help much.

Regards, Kiran

Developer
Nov 24, 2010 at 6:16 PM

Hi Kiran,

In Prism, one of the default behaviors attached to regions is the RegionActiveAwareBehavior. So if you implement IActiveAware in your views, they will be notified when the view is activated or deactivated (through the Region.Activate and Region.Deactivate methods), thus modifying the IsActive property of your view. When an object that implements IActiveAware changes its IsActive property, you could raise the IsActiveChanged event, so you can hook to that event and implement the necessary logic to cancel the action if needed.

That said, take into account that when you switch tabs in a TabControl Region, the views are being activated/deactivated accordingly, so the aforementioned seems to be a possible approach for achieving your scenario.

I hope you find this helpful.

Guido Leandro Maliandi
http://blogs.southworks.net/gmaliandi

Nov 25, 2010 at 5:27 PM

Hi,

I tried the following you mentioned, but its resulting in the blank screen displayed on the first pass. and after that it has no faffect. do you see anything wrong in my approach.

        private bool _isActive;
        public bool IsActive
        {
            get
            {
                return this._isActive;

            }
            set
            {
                if (this._isActive != value)
                {
                    this._isActive = value;
                    // Fire IsActiveChanged event
                    RaiseIsActiveChanged();//Your logic to fire the event
                    this.RaisePropertyChanged("IsActive");
                }
            }
        }

        public event EventHandler IsActiveChanged;

        #endregion
        public UpdateView MainView { get; set; }

        private void RaiseIsActiveChanged()
        {
            var handler = this.IsActiveChanged;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
        private void MasterDataViewModel_IsActiveChanged(object sender, EventArgs e)
        {
            if (IsActive == false)
            {
                //bool allowNavigation = DataService.CanNavigateOutOfView();
                bool allowNavigation = false;
                if (!allowNavigation)
                {
                    iRegionManager.Regions["MainRegion"].Activate(MainView);
                    //this.VisibilityService.EnterViewAnimation(ScreenTransitionAnimation.GrowInOut, MainView, null);
                }
            }
        }

in this sample MainView is the usercontrol that needs t o be activated and I for my testing I am setting its value while assigning the data context to the view. I can't understand what else needs to be done.

Regards, Kiran

 

Nov 26, 2010 at 7:27 AM

Hi Kiran,

Please let me know if you implement this successfully.

Thanks.

Nov 26, 2010 at 7:42 AM

well I had no luck in getting it working...

Regards, Kiran

Nov 26, 2010 at 7:49 PM

Hi,

Based on my understanding, you are trying to prevent the switching of a TabItem. I created a spike for achieving this scenario, so you can download it from here.

The application adds a handler to the Views.CollectionChanged event in the region (MainRegion) and contains 3 tabitems, so when the user tries to switch from the middle tab (HelloWorld1 view) to any other, it shows a confirmation dialog. If the user click Cancel button, it will prevent the tab switching, as shown below.

public partial class Shell : UserControl
{
private readonly IRegionManager _regionManager;
private IRegion _mainRegion;
private UserControl _dirtyView;

public Shell(IRegionManager regionManager)
{
InitializeComponent();
_regionManager = regionManager;
_regionManager.Regions.CollectionChanged += Regions_CollectionChanged;
}

private void Regions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (var region in e.NewItems)
{
var newRegion = (Region)region;
if (newRegion.Name == "MainRegion")
{
_mainRegion = newRegion;
_mainRegion.ActiveViews.CollectionChanged += Views_CollectionChanged;
_regionManager.Regions.CollectionChanged -= Regions_CollectionChanged;
}
}
}

private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{

if (e.Action == NotifyCollectionChangedAction.Remove)
{
var possibleDirtyView = e.OldItems[e.OldStartingIndex] as UserControl;

_dirtyView = null;
// check whether your view is dirty
if (possibleDirtyView != null &&
possibleDirtyView.GetType() == typeof(HelloWorldModule.Views.HelloWorld1))
{
var result = MessageBox.Show("Are you sure?", "Confirmation", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.Cancel)
{
_dirtyView = possibleDirtyView;
}
}
}

//if you have a dirty view active it
if (e.Action == NotifyCollectionChangedAction.Add
&& _dirtyView != null)
{
_mainRegion.Activate(_dirtyView);
}
}

}

Hope this helps.

Fernando Antivero
http://blogs.southworks.net/fantivero

 

 

Nov 29, 2010 at 6:32 PM

Fernando Antivero,

This is exactly what I was looking for. Thanks for helping me out again.

Regards, Kiran

 

Mar 15, 2011 at 11:42 PM

Hi,

I am trying to get this solution to work, and although it seems fine in silverlight i can't seem to get it to work in WPF.  The call to Activate() on the region does not seem to cause the view to stay on the current tab.  Any ideas?

 

Cheers

Developer
Mar 16, 2011 at 8:26 PM

Hi,

We've been able to reproduce the issue you've mentioned. From our first impression, this seems to be related to a possible timing issue in the SelectorItemsSourceSyncBehavior, which (from its summary):

"(...) keeps the items of the <see cref="Selector"/> host control in synchronization with the <see cref="IRegion"/>. This behavior also makes sure that, if you activate a view in a region, the SelectedItem is set. If you set the SelectedItem or SelectedItems (ListBox) then this behavior will also call Activate on the selected items. When calling Activate on a view, you can only select a single active view at a time. By setting the SelectedItems property of a listbox, you can set multiple views to active."

The behavior subscribes to the Region's ActiveViews CollectionChanged event and the hosting selector's SelectionChanged event (which in your case is a tab control) in order to match them. However, when manually setting the SelectedItem, the ActiveViews collection is also modified, so in order not to fire the event twice, a boolean field called updatingActiveViewsInHostControlSelectionChanged is set to true, so that the handler for the ActiveViews collection changed event does no action (avoiding a loop that could lead to an unexpected behavior). After the synchronization has been made, the updatingActiveViewsInHostControlSelectionChanged is set back to false.

The problem we've found is that, when you switch to a view and click "Cancel", the HostControlSelectionChanged method is called to synchronize the Region's ActiveViews collection (which sets the updatingActiveViewsInHostControlSelectionChanged value to true), and immediately after, the Activate method is called on the Region, before the HostControlSelectionChanged has reached the finally statement in which the updatingActiveViewsInHostControlSelectionChanged is set back to false, so the change in the active view caused by calling the Activate method is not displayed, since it's not synchronized with the view.

To illustrate this, here's a fragment of the code from the HostControlSelectionChanged and ActiveViews_CollectionChanged methods in the SelectorItemsSourceSyncBehavior:

        private void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (this.updatingActiveViewsInHostControlSelectionChanged)
            {
                // If we are updating the ActiveViews collection in the HostControlSelectionChanged, that
                // means the user has set the SelectedItem or SelectedItems himself and we don't need to do that here now
                return;
            }

            (...)
        }

        private void HostControlSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            try
            {
                // Record the fact that we are now updating active views in the HostControlSelectionChanged method.
                // This is needed to prevent the ActiveViews_CollectionChanged() method from firing.
                this.updatingActiveViewsInHostControlSelectionChanged = true;

                (...)
            }
            finally
            {
                this.updatingActiveViewsInHostControlSelectionChanged = false;
            }
        }

One possible workaround for this would be to handle the dirty view confirmation directly at a control level, not passing through the region. That would imply, for example, subscribing to the TabControl's SelectionChanged event to handle the confirmation.

I hope you find this helpful.

Guido Leandro Maliandi
http://blogs.southworks.net/gmaliandi