c# – Refactoring XAML Markup

Question:

I am reading Robert Martin and trying to comprehend all the subtleties of refactoring.

If with C # code everything is more or less clear and the code gradually starts to please the eye, then with XAML everything is sad. The markup is cumbersome and difficult to read. It is clear that a lot has to do with xml- "inheritance". Xml is redundant and not very pleasing to the eye, but still. What are the ways and who uses what to refactor xaml?

Ideally, I would like to get rid of the ten-level hierarchy and get short "methods" of 5-7 lines (as I have done in c # -code).

Here is the markup for one of the windows of my last WPF application (all 320 lines of "mince"):

<Window x:Class="InfoReceiverB.Views.EditorWindow"
        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:InfoReceiverB.Views"
        mc:Ignorable="d"
        Title="Менеджер скриптов" Icon="/papers2.ico"
        WindowStartupLocation="CenterScreen"
        Height="700" Width="1100">

    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="2*"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="3*"/>
                </Grid.RowDefinitions>

                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>

                    <ListBox Padding="2.5" Margin="3"
                             ItemsSource="{Binding Repo.Queries}"
                             SelectedItem="{Binding SelectedQuery}">
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding Id}" Margin="0,0,2,0"/>
                                    <TextBlock Text="{Binding Caption}"/>
                                </StackPanel>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                    </ListBox>

                    <StackPanel Grid.Column="1"
                                Margin="0,3,3,3">
                        <Button Content="+"
                                Padding="5" Margin="0,0,0,3"
                                Command="{Binding AddQueryCommand}"/>

                        <Button Content="-"
                                Padding="5" Margin="0,0,0,3"
                                Command="{Binding DelQueryCommand}"
                                CommandParameter="{Binding SelectedQuery}">
                            <Button.Style>
                                <Style TargetType="Button">
                                    <Style.Triggers>
                                        <Trigger Property="CommandParameter" Value="{x:Null}">
                                            <Setter Property="IsEnabled" Value="False"/>
                                        </Trigger>
                                    </Style.Triggers>
                                </Style>
                            </Button.Style>
                        </Button>
                    </StackPanel>
                </Grid>

                <GridSplitter Grid.Row="1" Height="3"
                              HorizontalAlignment="Stretch"
                              Background="DarkGray" Margin="3,0"/>

                <Grid Grid.Row="2" DataContext="{Binding SelectedQuery}">
                    <Grid.Style>
                        <Style TargetType="Grid">
                            <Style.Triggers>
                                <Trigger Property="DataContext" Value="{x:Null}">
                                    <Setter Property="IsEnabled" Value="False"/>
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </Grid.Style>

                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>

                    <TextBlock Text="{Binding Id}"
                               Padding="5" Margin="3,3,0,3"/>

                    <TextBox Grid.Column="1"
                             Text="{Binding Caption}"
                             Padding="5" Margin="0,3,3,3"
                             AcceptsReturn="True"/>

                    <TextBox Grid.Row="1" Grid.ColumnSpan="2"
                             Text="{Binding Body}" 
                             FontFamily="Consolas" FontSize="14"
                             Padding="5" Margin="3,0,3,3"
                             AcceptsReturn="True" AcceptsTab="True"
                             HorizontalScrollBarVisibility="Auto"
                             VerticalScrollBarVisibility="Auto"/>
                </Grid>
            </Grid>

            <GridSplitter Grid.Column="1" Width="3"
                          HorizontalAlignment="Stretch"
                          Background="DarkGray" Margin="0,3"/>

            <Grid Grid.Column="2">
                <Grid.RowDefinitions>
                    <RowDefinition Height="2*"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="3*"/>
                </Grid.RowDefinitions>

                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>

                    <ListBox Padding="2.5" Margin="3"
                             ItemsSource="{Binding Repo.QueryPacks}"
                             SelectedItem="{Binding SelectedQueryPack}">
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding Id}" Margin="0,0,2,0"/>
                                    <TextBlock Text="{Binding Caption}"/>
                                </StackPanel>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                    </ListBox>

                    <StackPanel Grid.Column="1"
                                Margin="0,3,3,3">
                        <Button Content="+"
                                Padding="5" Margin="0,0,0,3"
                                Command="{Binding AddQueryPackCommand}"/>

                        <Button Content="-"
                                Padding="5" Margin="0,0,0,3"
                                Command="{Binding DelQueryPackCommand}"
                                CommandParameter="{Binding SelectedQueryPack}">
                            <Button.Style>
                                <Style TargetType="Button">
                                    <Style.Triggers>
                                        <Trigger Property="CommandParameter" Value="{x:Null}">
                                            <Setter Property="IsEnabled" Value="False"/>
                                        </Trigger>
                                    </Style.Triggers>
                                </Style>
                            </Button.Style>
                        </Button>
                    </StackPanel>
                </Grid>

                <GridSplitter Grid.Row="1" Height="3"
                              HorizontalAlignment="Stretch"
                              Background="DarkGray" Margin="3,0"/>

                <Grid Grid.Row="2" DataContext="{Binding SelectedQueryPack}">
                    <Grid.Style>
                        <Style TargetType="Grid">
                            <Style.Triggers>
                                <Trigger Property="DataContext" Value="{x:Null}">
                                    <Setter Property="IsEnabled" Value="False"/>
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </Grid.Style>

                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>

                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>

                        <TextBlock Text="{Binding Id}"
                                   Padding="5" Margin="3,3,0,3"/>

                        <TextBox Grid.Column="1"
                                 Text="{Binding Caption}"
                                 Padding="5" Margin="0,3,3,3"/>
                    </Grid>

                    <Grid Grid.Row="1">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>

                        <ListBox Name="list1" Padding="2.5" Margin="3"
                                 ItemsSource="{Binding DataContext.Repo.Queries,
                                                       RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <TextBlock Text="{Binding Id}" Margin="0,0,2,0"/>
                                        <TextBlock Text="{Binding Caption}"/>
                                    </StackPanel>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>

                        <StackPanel Grid.Column="1"
                                    Margin="0,3" VerticalAlignment="Center">
                            <Button Content="&gt;"
                                    Padding="5" Margin="0,0,0,3"
                                    Command="{Binding DataContext.AddQueryToPackCommand,
                                                      RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
                                    CommandParameter="{Binding SelectedItem, ElementName=list1}">
                                <Button.Style>
                                    <Style TargetType="Button">
                                        <Style.Triggers>
                                            <Trigger Property="CommandParameter" Value="{x:Null}">
                                                <Setter Property="IsEnabled" Value="False"/>
                                            </Trigger>
                                        </Style.Triggers>
                                    </Style>
                                </Button.Style>
                            </Button>

                            <Button Content="&lt;"
                                    Padding="5" Margin="0,7,0,3"
                                    Command="{Binding DataContext.DelQueryFromPackCommand,
                                                      RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
                                    CommandParameter="{Binding SelectedItem, ElementName=list2}">
                                <Button.Style>
                                    <Style TargetType="Button">
                                        <Style.Triggers>
                                            <Trigger Property="CommandParameter" Value="{x:Null}">
                                                <Setter Property="IsEnabled" Value="False"/>
                                            </Trigger>
                                        </Style.Triggers>
                                    </Style>
                                </Button.Style>
                            </Button>

                            <Button Content="^"
                                    Padding="5" Margin="0,0,0,3"
                                    Command="{Binding DataContext.UpQueryInPackCommand,
                                                      RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
                                    CommandParameter="{Binding SelectedItem, ElementName=list2}">
                                <Button.Style>
                                    <Style TargetType="Button">
                                        <Style.Triggers>
                                            <Trigger Property="CommandParameter" Value="{x:Null}">
                                                <Setter Property="IsEnabled" Value="False"/>
                                            </Trigger>
                                        </Style.Triggers>
                                    </Style>
                                </Button.Style>
                            </Button>

                            <Button Content="v"
                                    Padding="5" Margin="0,0,0,3"
                                    Command="{Binding DataContext.DownQueryInPackCommand,
                                                      RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
                                    CommandParameter="{Binding SelectedItem, ElementName=list2}">
                                <Button.Style>
                                    <Style TargetType="Button">
                                        <Style.Triggers>
                                            <Trigger Property="CommandParameter" Value="{x:Null}">
                                                <Setter Property="IsEnabled" Value="False"/>
                                            </Trigger>
                                        </Style.Triggers>
                                    </Style>
                                </Button.Style>
                            </Button>
                        </StackPanel>

                        <ListBox Grid.Column="2" Name="list2"
                                 Padding="2.5" Margin="3"
                                 ItemsSource="{Binding Queries}">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <TextBlock Text="{Binding Id}" Margin="0,0,2,0"/>
                                        <TextBlock Text="{Binding Caption}"/>
                                    </StackPanel>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </Grid>                    
                </Grid>
            </Grid>
        </Grid>

        <UniformGrid Grid.Row="1" HorizontalAlignment="Right"
                     Rows="1">
            <Button Grid.Column="1"
                    Content="Применить" IsDefault="True"
                    Padding="5" Margin="0,3,3,3"
                    Command="{Binding ApplyCommand}"
                    Click="ApplyButtonClick"/>

            <Button Grid.Column="2"
                    Content="Отмена" IsCancel="True"
                    Padding="5" Margin="0,3,3,3"/>
        </UniformGrid>
    </Grid>
</Window>

I will be glad to any recommendations and comments, especially with examples

Answer:

The usual alternative is to separate the common logical parts into separate code.

If you have parts of markup with independent meaning, independent functionality, they should be moved to a separate UserControl . If, moreover, chunks and repetitive – you can reuse your UserControl . (But even without this, if there is an independent meaning, you need to separate it into a new control – just like with procedures.) If the parts are repeated, but not exactly, make your UserControl parameterizable to customize its appearance and behavior. This is a complete analogue of using subroutines (functions / procedures) in imperative programming. Without UserControl your code becomes an unsupported monolith, just like imperative code without procedures.

Further, if your controls have common properties, put them in the style. This can be a little more complicated than just writing properties in all controls, but a properly written style can be reused, inherited and modified. You can create style hierarchies and apply them to your controls to modify their behavior in the same way. It is analogous to AOP in imperative programming.

PS: This will not give you 10-line parts, since XML is still quite verbose and does not allow multiple entities in the same file. I think a realistic goal might be 30-50 lines per file.

Scroll to Top