locked
How to Cancel Navigation between TabItems in a TabControl? RRS feed

  • Question

  •  

    Wow - one would think this would be easier than it has been.  I am now to the point where I need to solicit input from the forums while I go work on other things...

     

    I am trying to set up a situation where a user will not be allowed to navigate away from the current tab unless some condition has been met (false == IsDirty, etc.)  The problem is, there seems to be no PreviewSelectionChanged event on the Tab control (nor does there seem to be any analog thereof.)  I have seen solutions where ther "trick" is to override PreviewLeftMouseClick (http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=940143&SiteID=1) which falls down as soon as Ctrl-Click or Tab and right/left arrow are used to execute tab navigation.

     

    I have tried 2 obscure approaches, with no success.  First, I tried subclassing the TabControl and overriding the DependencyProperty metadata for the SelectedItem DP so that I could handle the "prevent navigation" situation in the Coercion callback. 

    Code Block

    class SpecializedTabControl : TabControl

    {

    static Boolean gateKeeper = false;

    static SpecializedTabControl()

    {

    FrameworkPropertyMetadata itemMetaData = new FrameworkPropertyMetadata(new PropertyChangedCallback(SelectedItemChanged), new CoerceValueCallback(CoerceSelectedItem));

    TabControl.SelectedItemProperty.OverrideMetadata(typeof(SpecializedTabControl), itemMetaData);

    }

     

    static void SelectedItemChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)

    {

    }

     

    static object CoerceSelectedItem(DependencyObject o, Object value)

    {

    object result = value;

    if (gateKeeper) {

    SpecializedTabControl tabControl = o as SpecializedTabControl;

    if (null != tabControl) {

    result = DependencyProperty.UnsetValue;// tabControl.SelectedItem;

    }

    }

    return result;

    }

     

     

    Note that the static bool gateKeeper value, above, is just used so I can quickly simulate the desired failure condition in the debugger.

     

    Anyway, when I change tabs, I hit the expected callbacks, and am able to "reset" the value in the Coercion callback.  however, the UI still changes.  If I subsequently try to navigate back to the original tab, I do not hit the callbacks at all, but the UI does update to show the original tab.

     

    I also thought I might have some luck with the TabControl.Items.CurrentChanging and TabControl.Items.CurrentChanged events.

     

    Code Block

    class SpecializedTabControl : TabControl

    {

    static Boolean gateKeeper = false;

    public SpecializedTabControl()

    {

    this.IsSynchronizedWithCurrentItem = true;

    }

     

    protected override void OnInitialized(EventArgs e)

    {

    base.OnInitialized(e);

    this.Items.CurrentChanging += new System.ComponentModel.CurrentChangingEventHandler(Items_CurrentChanging);

    this.Items.CurrentChanged += new EventHandler(Items_CurrentChanged);

    }

     

    void Items_CurrentChanging(object sender, System.ComponentModel.CurrentChangingEventArgs e)

    {

    if (gateKeeper) {

    e.Cancel = true;

    }

    }

     

    void Items_CurrentChanged(object sender, EventArgs e)

    {

    }

     

     

    This fails in a very similar way.  Looking at the underlying objects, I can see that there is a CurrentItem kept on the internal collection within the TabItem, which does reflect the changes, however the UI does not.  Setting IsSynchronizedWithCurrentItem has no effect.

     

    Any insights/ideas?  My next alternative is probably to switch to make my own pseudo-TabControl with buttons and panels.

     

    Wednesday, December 12, 2007 8:33 PM

Answers

  • John,

     

    I feel your pain.  I went through this same process when writing my applicaiton close code.  I didn't want the user to be able to press the famous little Red X and not be notified if their forms were dirtry or not.

     

    I do allow my users to switch tabs without saving but ensure that each dirry form is clean or changes cancelled prior to closing.

     

    For your situation, you can use a similar approach that I took with almost no code. 

     

    Step 1.  Figure out which UI Element is responsible for knowing if your form is dirty or not.  This can be the TabItem or the TabItems content.

     

    What I did, was to ensure that ANY parent container element placed in the TabItem implments a simple interface.  In my case it is the IApplicationForm.  This way, I'm coding to an interface and not to implementation.  All my TabItems now work exactly the same.

     

    Step 2.  On TabItem selection changed, or application closing, etc. just query the TabItem and if it is dirty then do something.  In your case, you want them back on your tab.  If this was a hard and fast rule, you could also disable the other tabs when any fom got dirty.  Just fire off a routed event and let the TabControl service it and disable all the other tabs other than the source tab.

     

    Here is my function that is called when the application wants to close.  I could have 10 tabs open, and this little gem does everything for me.

     

    In my case, it loops through all the TabItems.  Displays them.  If the TabItem is dirty it fires off the TabItems CloseForm function, which either saves the form or cancels the closing of the application.

     

    You should be able to doctor this code to accomplish exactly what you are looking to do.

     

    Friend Function CanCloseApplication() As Boolean

      For Each ti As TabItem In Me.tcOpenPages.Items

        Dim objIApplicationObject As Core.IApplicationForm = TryCast(ti.Content, IApplicationForm)

        If objIApplicationObject IsNot Nothing Then

          Me.tcOpenPages.SelectedItem = ti

          UpdateLayout()

          ti.Focus()

          If objIApplicationObject.CloseForm = False Then

              Return False

          End If

        End If

      Next

      Return True

    End Function

     

    Public Interface IApplicationForm

      Sub OpenRecord(ByVal objControlKeyValue As Object)

      Function CloseForm() As Boolean

      Property IsParentContainerPopUpWindow() As Boolean

      Event CloseParentContainerPopUpWindow(ByVal sender As Object, ByVal e As EventArgs)

    End Interface

     

    Have a nice day,

     

    Karl

    Wednesday, December 12, 2007 9:24 PM

All replies

  • You should be able to check if the particular keys are down in the PreviewLeftMouseClick event  and cancel the navigation. (Note- did not try it myself)

     

    Wednesday, December 12, 2007 9:17 PM
  • Thanks for the reply Lee.  It is not so much a matter of being concerned about whether keys are down or not.  The LeftMouseClick is not a proper solution because the mouse is not the only way to change which tab is selected in a tab control - it is technically possible to navigate a tab control without a mouse even being connected ot the system in the first place.

     

    For example, Ctrl-Click will move to the next tab, and you can also use th eTab key to navigate to the currently selected tab.  Once there, the left and right arrow keys on the keyboard can be used for tab selection.

     

    Wednesday, December 12, 2007 9:23 PM
  • John,

     

    I feel your pain.  I went through this same process when writing my applicaiton close code.  I didn't want the user to be able to press the famous little Red X and not be notified if their forms were dirtry or not.

     

    I do allow my users to switch tabs without saving but ensure that each dirry form is clean or changes cancelled prior to closing.

     

    For your situation, you can use a similar approach that I took with almost no code. 

     

    Step 1.  Figure out which UI Element is responsible for knowing if your form is dirty or not.  This can be the TabItem or the TabItems content.

     

    What I did, was to ensure that ANY parent container element placed in the TabItem implments a simple interface.  In my case it is the IApplicationForm.  This way, I'm coding to an interface and not to implementation.  All my TabItems now work exactly the same.

     

    Step 2.  On TabItem selection changed, or application closing, etc. just query the TabItem and if it is dirty then do something.  In your case, you want them back on your tab.  If this was a hard and fast rule, you could also disable the other tabs when any fom got dirty.  Just fire off a routed event and let the TabControl service it and disable all the other tabs other than the source tab.

     

    Here is my function that is called when the application wants to close.  I could have 10 tabs open, and this little gem does everything for me.

     

    In my case, it loops through all the TabItems.  Displays them.  If the TabItem is dirty it fires off the TabItems CloseForm function, which either saves the form or cancels the closing of the application.

     

    You should be able to doctor this code to accomplish exactly what you are looking to do.

     

    Friend Function CanCloseApplication() As Boolean

      For Each ti As TabItem In Me.tcOpenPages.Items

        Dim objIApplicationObject As Core.IApplicationForm = TryCast(ti.Content, IApplicationForm)

        If objIApplicationObject IsNot Nothing Then

          Me.tcOpenPages.SelectedItem = ti

          UpdateLayout()

          ti.Focus()

          If objIApplicationObject.CloseForm = False Then

              Return False

          End If

        End If

      Next

      Return True

    End Function

     

    Public Interface IApplicationForm

      Sub OpenRecord(ByVal objControlKeyValue As Object)

      Function CloseForm() As Boolean

      Property IsParentContainerPopUpWindow() As Boolean

      Event CloseParentContainerPopUpWindow(ByVal sender As Object, ByVal e As EventArgs)

    End Interface

     

    Have a nice day,

     

    Karl

    Wednesday, December 12, 2007 9:24 PM
  • This is an awful idea, I'm not surprised it's hard or impossible. When did you last see this done in Windows? You didn't. You shouldn't ever try to stop a user from changing to a tab, it's a horrible UI practice.

     

    Wednesday, December 12, 2007 11:06 PM
  •  

    Tim,

     

    He is trying to keep the user on the current tab until the information on the tab is complete. 

     

    What is wrong with that?  Data validation is a good thing right?

     

    Karl

     

     

    Thursday, December 20, 2007 6:40 AM
  • Tim - I wish you would have given your reply more thought prior to its submission.  You are correct in that when Tabs are used to group logically related concepts into a limited amount of space (for example, property sheets), that this approach is flawed.  All the data is committed or none of it is committed.

     

    However, there are many more uses for tabs, especially in a WPF environment, where controls may not always look like they did in WinForms and earlier.  A metaphor that is not precisely the one I am working in, but which may help to make my point, is using tabs to represent "sheets of paper" in a notebook (especially when the tab is docked along one of the sides instead of just along the top.)  In such a case, it may be proper to save each page before navigating to another, otherwise you could present a really obnoxious UI when closing the applicaiton (all depending on how the applicaiton is to be used.)

     

    Another example that I have seen and previously worked on is many common video editing applications, where tabs (or the simulation therof) are used to segment the UI into work areas.  Capturing a media file is a very different activity from editing a sequence.  When moving away from Editing and into Capture, it may be proper to remind the user to save their work.  Otherwise, if they exit from Capture, they'll be prompted to save something they may not have worked on in hours (days?)

     

    I hope that helped answer the question you asked.

    Thursday, December 20, 2007 2:36 PM
  • Easiest way I've found so far is:

    private void Window_Loaded(object senderRoutedEventArgs e)
    {
      tabControl.Items.CurrentChanging += new CurrentChangingEventHandler(Items_CurrentChanging);
    }

    bool IsValid = false;
    void Items_CurrentChanging(object senderCurrentChangingEventArgs e)
    {
      if (e.IsCancelable && !IsValid)
      {
        var item = ((ICollectionView)sender).CurrentItem;
        e.Cancel = true;
        tabControl.SelectedItem = item;
      }
    }

    Works like a charm. 


    Shimmy
    Tuesday, November 16, 2010 11:35 PM