Dynamic splitter control for Universal Windows App (UWP)

I developed some prototype of a custom layout panel on Universal Windows Platform called DynamicSplitter that contains multiple child panes and arranges them horizontally or vertically depending on its IsHorizontal property of type bool. To support heterogeneous layout of panes, DynamicSplitter itself is used as the child pane of another DynamicSplitter with different IsHorizontal property. So, for example, the parent splitter can have two panes split horizontally one of which is a user control and other is the child splitter, and the child splitter can have three panes split vertically and so on.

Regardless of DynamicSplitter has a lot in common with Grid or StackPanel it is inherited directly from Panel and lays out its child panes by overriding MeasureOverride(…) and ArrangeOverride(…) functions:

public ref class DynamicSplitter sealed : public Windows::UI::Xaml::Controls::Panel
{
public:

    DynamicSplitter();

    typedef Windows::UI::Xaml::FrameworkElement Pane;

...

protected:

    Size MeasureOverride(Size availableSize) override;

    Size ArrangeOverride(Size finalSize) override;

    //factory methods for creating child panes and child splitters

    virtual Pane ^ CreatePane();

    virtual DynamicSplitter ^ CreateSplitter();
...

private:

    static const int TRACK_BAR_THICKNESS = 8;

    bool bHorizontal;

    typedef std::vector<float> FloatVector;

    //array containing relative sizes of the panes (ratios), 
    //sum of all the ratios is 1.0, used to resize the panes proportionally.
    FloatVector Ratios;

    property bool IsHorizontal
    {
        bool get()
        {
            return bHorizontal;
        }
    }

    int GetPaneCount() const
    {
        return Ratios.size();
    }

    void ArrangePanes(Size finalSize)
    {
        MeasureOrArrangePanes(finalSize, false);
    }
    
    void MeasurePanes(Size finalSize)
    {
        MeasureOrArrangePanes(finalSize, true);
    }
    
    void MeasureOrArrangePanes(Size size, bool measure);
...
}

MeasureOverride(…) and ArrangeOverride(…) functions call MeasureOrArrangePanes(…) internally that calls Measure(…) or Arrange(…) on child panes depending on measure parameter:

Size DynamicSplitter::MeasureOverride(Size availableSize)
{
    MeasurePanes(availableSize);
    
    return availableSize; //did not work before
}

Size DynamicSplitter::ArrangeOverride(Size finalSize)
{
    ArrangePanes(finalSize);
    
    return __super::ArrangeOverride(finalSize);
}


void DynamicSplitter::MeasureOrArrangePanes(Size size, bool measure)
{
    //float unity = std::accumulate(Ratios.begin(), Ratios.end(), 0.0F);

    //total length of all the panes with the track bars
    float total_length = (IsHorizontal ? size.Height : size.Width);

    const float left_or_top_org = 0;

    float sum_ratio = 0;

    for (int i = 0; i < (int)Ratios.size(); ++i)
    {
        float ratio = Ratios[i];

        float left_or_top = left_or_top_org + sum_ratio * total_length;

        float beg = left_or_top;

        if (i != 0)
        {
            beg += TRACK_BAR_THICKNESS / 2;
        }

        float len = total_length * ratio - TRACK_BAR_THICKNESS;

        if (i == 0)
        {
            len += TRACK_BAR_THICKNESS / 2;
        }

        //0 could be the last pane if there is only one pane
        if (i == (int)Ratios.size() - 1)
        {
            //the last len accumulates all the inacurracy
            len = total_length - beg;
        }

        Rect rcPane;

        if (IsHorizontal)
        {
            rcPane = Rect(Point(0, beg), Point(size.Width, beg + len));
        }
        else
        {
            rcPane = Rect(Point(beg, 0), Point(beg + len, size.Height));
        }

        //it can be either Splitter or Pane
        Pane ^ pane = GetPaneByIndex(i);
        
        if (measure)
        {
            pane->Measure(Size(rcPane.Width, rcPane.Height));
        }
        else
        {
            pane->Arrange(rcPane);
        }

        sum_ratio += ratio;
    }
}

This code works nicely, at the picture below you can see three panes with the red border:

Dynamic splitter control for Windows Store App (WinRT)

All the panes are of the same type derived from UserControl and contain ListView with cyan border, ItemsControl with yellow border and buttons for adding/deleting items. Buttons at the top-right corner of the pane are used to split the pane horizontally (H) or vertically (V) and to close the pane (X).  If I add 1000 (1K) items to ListView and ItemsControl I can can scroll the lists with vertical scroll bar:

ListView and ItemsControl vertical scroll bar

The rest of the article describes a bug that Microsoft fixed with some Windows 10 update, so ItemsControl does not glitch with DynamicSplitter anymore and you can stop reading here.

So we have some beautiful pictures and working controls, but the the only issue is that I face some terrible bug with scrolling of ItemsControl: If I close second pane and so cause the execution of code that does some manipulation with the visual tree (see below) the vertical scrolling of ItemsControl stops working, and an empty space appears instead of ItemsControl rows:

ItemsControl does not generate new rows while scrolling anymore.

But resizing the main window a bit or adding an item to the collection bound to ItemsControl.ItemsSource make the scrolling work again, the empty space is filled with the rows and all items are scrolled fine:

Now everything is OK. The empty space is filled with the rows.

Below I provided the code that brakes my ItemsControl scrolling:

//pPane is the pane to be closed

DynamicSplitter ^ pParentSplitter = dynamic_cast<DynamicSplitter ^>(VisualTreeHelper::GetParent(this));

int nPaneIndex = GetPaneIndex(pPane);

int second_pane_index = nPaneIndex == 0 ? 1 : 0;

//pSecondPane contains our ItemsControl
//and we change its parent by removing from
//child splitter and adding to the parent splitter

Pane ^ pSecondPane = GetPaneByIndex(second_pane_index);

int my_index = pParentSplitter->GetPaneIndex(this);

Children->RemoveAt(second_pane_index);

pParentSplitter->Children->RemoveAt(my_index);

pParentSplitter->Children->InsertAt(my_index, pSecondPane);

pParentSplitter->InvalidateArrange();

Can anyone tell me what I did wrong? Or probably ItemsControl does not like to be moved across the parent panels? Below I provided the source code of my ItemsControl:

<!-- We Add Borders around each ListView Item, to make it look like a grid, you can change this. -->
<Style x:Key="ItemBorder" TargetType="Border">
    <Setter Property="BorderBrush" Value="Gray" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="Background" Value="White" />
</Style>

<Style x:Key="ColumnItemBorder" TargetType="Border">
    <Setter Property="BorderBrush" Value="Gray" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="Background" Value="Silver" />
</Style>

<Style TargetType="local:DataGrid" >
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:DataGrid">
                <Border
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                    <ScrollViewer Grid.Row="1" HorizontalScrollMode="Auto" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
                        <ScrollViewer.TopHeader>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="60" />
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="90" />
                                    <ColumnDefinition Width="90" />
                                </Grid.ColumnDefinitions>
                                <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="0">
                                    <TextBlock>Id</TextBlock>
                                </Border>
                                <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="1">
                                    <TextBlock>Name</TextBlock>
                                </Border>
                                <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="2">
                                    <TextBlock>LAT</TextBlock>
                                </Border>
                                <Border Style="{StaticResource ColumnItemBorder}" Grid.Column="3">
                                    <TextBlock>LON</TextBlock>
                                </Border>
                            </Grid>
                        </ScrollViewer.TopHeader>
                        <!-- Our ListView's Regular Rows. -->
                        <ItemsPresenter HorizontalAlignment="Left" VerticalAlignment="Top" />
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <!-- VirtualizingStackPanel produces this:
                WinRT information: The TopLeftHeader, TopHeader and LeftHeader properties cannot be used when the Content is an OrientedVirtualizingPanel,  
                VirtualizingStackPanel, CarouselPanel or WrapGrid instance.
                <VirtualizingStackPanel Orientation="Vertical"></VirtualizingStackPanel>
                so we use StackPanel
                -->
                <ItemsStackPanel Orientation="Vertical"></ItemsStackPanel>
                <!--<StackPanel Orientation="Vertical"></StackPanel>-->
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
</Style>

by the way, changing ItemsStackPanel with StackPanel solves the issue, but of cause the performance of StackPanel is not acceptable.

Leave a Reply

Your email address will not be published. Required fields are marked *