Tuesday, October 1, 2024

Breaking the IsEnabled inheritance

One useful feature of WPF is that when you disable a container, all the controls in that container are disabled too. So if you want to put a page into read-only mode, you just have to disabled the page. The upside is simplicity and security. The downside to this is that there are probably some buttons you want the user to be able to click on.

The common technique to get around this is to put the buttons in a different container. However, this does not solve the problems associated with grids, lists, multi-line text boxes and other controls that need to respond to user interactions without allowing the user to edit them.

I read an interesting response to a question on StackOverflow see the first answer here https://stackoverflow.com/questions/14584662/enable-a-child-control-when-the-parent-is-disabled. It struck me as a very clever solution and definitely not a hack. I rewrote it in Visual Basic and created a full solution.

The idea is to wrap a component that should inherits its parent IsEnabled property in a container that breaks that inheritance. It's really quite elegant. 

Start a WPF, VB, .Net Framework Visual Studio project and call it BreakEnabledInheritance. It should work for .Net Core just as well.

Add a class called BreakEnabledInheritanceContainer. The code is very simple.

Public Class BreakEnabledInheritanceContainer
    Inherits ContentControl

    Shared Sub New()
        IsEnabledProperty.OverrideMetadata(GetType(BreakEnabledInheritanceContainer),
                                           New UIPropertyMetadata(defaultValue:=True,
                                                                  propertyChangedCallback:=Sub(a, b)
                                                                                           End Sub,
                                                                  coerceValueCallback:=Function(a, b) b))
    End Sub

End Class

Now replace MainWindow.xaml with this. It creates two multi-line text boxes inside a disabled grid. The second text box is wrapped by a BreakEnabledInheritanceContainer and has IsReadOnly set true.
This leaves IsEnabled true and IsReadOnly true which allows the user to scroll it but not change it.

The first text box directly inherits the grid's IsEnabled so the user cannot interact with it at all.

<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:BreakEnabledInheritance"
        DataContext="{Binding RelativeSource={RelativeSource self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <Style TargetType="TextBox" x:Key="MultiLineTextBoxStyle" BasedOn="{StaticResource {x:Type TextBox}}">
            <Setter Property="TextWrapping" Value="Wrap"/>
            <Setter Property="AcceptsReturn" Value="True"/>
            <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="VerticalContentAlignment" Value="Top"/>
        </Style>
    </Window.Resources>
    <Grid IsEnabled="false" Height="30">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBox Grid.Column="0" Style="{StaticResource MultiLineTextBoxStyle}" Text="{Binding LongText}"/>
        <local:BreakEnabledInheritanceContainer Grid.Column="1">
            <TextBox Grid.Column="0" Style="{StaticResource MultiLineTextBoxStyle}" Text="{Binding LongText}" IsReadOnly="True"/>
        </local:BreakEnabledInheritanceContainer>
    </Grid>
</Window>

The code behind is trivial.
Class MainWindow
    Public Property LongText As String = "This is a long" & Environment.NewLine & "piece of text" & Environment.NewLine & "spread over several" & Environment.NewLine & "lines"
End Class

Here's the result...


This approach has the advantage that the preferred solution (controls inherit their parent's IsEnabled) can be used for most controls and we can override that functionality for selected controls. This reduces the chance of a developer accidently enabling a control that should be disabled and vice versa.

No comments:

Post a Comment