Keep visual state in tab control

Asked by At

I read, that the WPF tab control has the "feature" of unloading all content of the tab when the tab is changed and so not preserving the visual state of controls on the tab in question. But I understood that this "feature" is only active when working with ItemsSource of the tab control.

Now I have a tab control with explicit TabItems (not using ItemsSource) and the tab control still doesn't preserve the visual state.

Is there an error or is there something missing in my code or is it still this weird "feature" of the tab control? How can I preserve the visual state of the controls in my case?

Here's my sample code:

ViewModelBase.vb (implementing INotifyPropertyChanged and INotifyDataErrorInfo)

Imports System.ComponentModel

Public Class ViewModelBase
    Implements INotifyPropertyChanged
    Implements INotifyDataErrorInfo

    Private _lock As Object = New Object
    Private ReadOnly _errors As Dictionary(Of String, List(Of String)) = New Dictionary(Of String, List(Of String))

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Public Event ErrorsChanged As EventHandler(Of DataErrorsChangedEventArgs) Implements INotifyDataErrorInfo.ErrorsChanged

    Protected Overridable Sub OnErrorsChanged(propertyName As String)
        RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName))
    End Sub

    Protected Overridable Sub OnPropertyChanged(propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
        Me.ValidateAsync()
    End Sub

    Public ReadOnly Property HasErrors As Boolean Implements INotifyDataErrorInfo.HasErrors
        Get
            Return _errors.Any(Function(x As KeyValuePair(Of String, List(Of String))) (x.Value IsNot Nothing) AndAlso (x.Value.Count > 0))
        End Get
    End Property

    Public Function GetErrors(propertyName As String) As IEnumerable Implements INotifyDataErrorInfo.GetErrors
        If Not String.IsNullOrEmpty(propertyName) AndAlso _errors.ContainsKey(propertyName) Then
            Return _errors(propertyName)
        End If

        Return Nothing
    End Function

    Protected Sub SetErrors(ByVal propertyName As String, ByVal errors As List(Of String))
        If String.IsNullOrEmpty(propertyName) Then
            Exit Sub
        End If

        If (errors Is Nothing) OrElse (errors.Count = 0) Then
            If _errors.ContainsKey(propertyName) Then
                _errors.Remove(propertyName)
                Call Me.OnErrorsChanged(propertyName)
            End If
        Else
            If _errors.ContainsKey(propertyName) Then
                _errors(propertyName) = errors
            Else
                _errors.Add(propertyName, errors)
            End If

            Call Me.OnErrorsChanged(propertyName)
        End If
    End Sub

    Public Function ValidateAsync() As Task(Of Boolean)
        Return Task(Of Boolean).Run(Of Boolean)(AddressOf Me.ValidateInternal)
    End Function

    Public Function Validate() As Boolean
        SyncLock _lock
            Return Me.ValidateInternal()
        End SyncLock
    End Function

    Protected Overridable Function ValidateInternal() As Boolean
        Return False
    End Function

End Class

MainViewModel.vb

Public Class MainViewModel
    Inherits ViewModelBase

    Private _textTab1 As String
    Private _textTab2 As String

    Public Property TextTab1 As String
        Get
            Return _textTab1
        End Get
        Set(value As String)
            _textTab1 = value
            MyBase.OnPropertyChanged(NameOf(Me.TextTab1))
        End Set
    End Property

    Public Property TextTab2 As String
        Get
            Return _textTab2
        End Get
        Set(value As String)
            _textTab2 = value
            MyBase.OnPropertyChanged(NameOf(Me.TextTab2))
        End Set
    End Property

    Protected Overrides Function ValidateInternal() As Boolean
        Dim result As Boolean
        Dim textTab1Errors As List(Of String)
        Dim textTab2Errors As List(Of String)

        result = MyBase.ValidateInternal()

        textTab1Errors = New List(Of String)
        textTab2Errors = New List(Of String)

        If Not String.IsNullOrEmpty(Me.TextTab1) AndAlso (Me.TextTab1.Length > 30) Then
             textTab1Errors.Add("The text on tab 1 is longer than 30 characters.")
             result = True
        End If

        MyBase.SetErrors(NameOf(Me.TextTab1), textTab1Errors)

        If Not String.IsNullOrEmpty(Me.TextTab2) AndAlso (Me.TextTab2.Length > 40) Then
             textTab2Errors.Add("The text on tab 2 is longer than 40 characters.")
             result = True
        End If

        MyBase.SetErrors(NameOf(Me.TextTab2), textTab2Errors)

        Return result
    End Function

End Class

MainWindow.xaml

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TabTest"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance Type=local:MainViewModel, IsDesignTimeCreatable=True}"
        Title="MainWindow" Height="200" Width="400">

    <Window.DataContext>
        <local:MainViewModel TextTab1="This is just a test." TextTab2="And this is another test." />
    </Window.DataContext>

    <TabControl IsSynchronizedWithCurrentItem="True">
        <TabItem Header="Tab 1">
            <TextBox AcceptsReturn="True" VerticalScrollBarVisibility="Auto" Text="{Binding TextTab1, UpdateSourceTrigger=PropertyChanged}" />
        </TabItem>
        <TabItem Header="Tab 2">
            <TextBox AcceptsReturn="True" VerticalScrollBarVisibility="Auto" Text="{Binding TextTab2, UpdateSourceTrigger=PropertyChanged}" />
        </TabItem>

    </TabControl>
</Window>

If you run the program you'll see a tab control with two tabs. The text on the first tab is limited to 30 characters the text on the second tab is limited to 40 characters.

If you keep adding characters to the text on the first tab you'll see a red border when you exceed the allowed 30 characters. When you then switch to the second tab and back to the first tab, the red border is gone although the reason for the error (more than 30 characters) still exists.

What can I do to keep the red border even when switching the tabs?

0 Answers