Wednesday, December 18, 2013

Preventing the user from changing the tab

A common scenario for applications is to keep track of unsaved changes and prevent the user from accidentally losing the changes by moving off the page or closing the application. This is commonly known as Dirty Page logic.

There are several ways to approach it - our standard is to warn the user and give them a chance to remain on the dirty page. Our pages are broken up into a Search tab, an Add tab, an Edit tab, etc. If the user has modified data on an Add or Edit tab and clicks on another tab we want to display a dialog box and give them an opportunity to stay on the current tab.

In WPF the only event available that is consistently raised when a user tries to change tabs on a tab control is the SelectionChanged event. This is raised AFTER the selection has changed so if you want to keep the user on the current tab you have to set the tab control's SelectedIndex back to what it was BEFORE the event was changed. This requires you to track the current index. No big deal. The real problem is that setting the SelectedIndex back only works a few times, then stops. Try this XAML and code behind...


<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
        </Grid.RowDefinitions>
        <TabControl Name="t1" Grid.Row="0" SelectionChanged="t1_SelectionChanged_1" >
            <TabItem Name="Search" Header="Search">
                <CheckBox Name="CanEditCheckbox" Content="Can Edit?" ></CheckBox>
            </TabItem>
            <TabItem Name="Edit" Header="Edit">
                <Label Name="EditLabel" Content="Edit"></Label>
            </TabItem>
        </TabControl>
        <Label Name="MessageLabel" Grid.Row="1"/>
    </Grid>
</Window>


Class MainWindow

    Private Sub t1_SelectionChanged_1(sender As Object, e As SelectionChangedEventArgs)

        If t1.SelectedIndex = 1 Then
            If Not CanEditCheckbox.IsChecked Then
                t1.SelectedIndex = 0
                MessageLabel.Content = "No Edit You! " & Now()
            Else
                MessageLabel.Content = "OK You Edit Now! " & Now()
            End If
        End If
    End Sub

End Class


Each time you click on the [Edit] tab you will see a message saying "No Edit You!" with the time after it. However after you have clicked it two or three times the time stops updating. In fact, the SelectionChanged event handler is no longer being called. That's because we're changing the SelectedIndex back to zero on the same thread. It immediately calls the SelectionChanged event handler again and stuff gets very messy.

To fix the problem we have to set the index back in a background thread. Fortunately WPF makes this fairly easy. Try changing the code behind like this (note the new Import)...


Imports System.Windows.Threading

Class MainWindow

    Private Sub t1_SelectionChanged_1(sender As Object, e As SelectionChangedEventArgs)

        If t1.SelectedIndex = 1 Then
            If Not CanEditCheckbox.IsChecked Then
                Application.Current.Dispatcher.BeginInvoke(DirectCast(Sub() t1.SelectedIndex = 0, Action), DispatcherPriority.Render, Nothing)
                MessageLabel.Content = "No Edit You! " & Now()
            Else
                MessageLabel.Content = "OK You Edit Now! " & Now()
            End If
        End If
    End Sub

End Class


All we did was create a lambda function to change the SelectedIndex and run it in a lower priority background thread. It doesn't get executed until our event handler is completed and nothing gets messed up.

1 comment:

  1. Awesome solution, thank you so much! BeginInvoke was exactly what I needed! (C#)

    ReplyDelete