Artykuł

freeimages.com freeimages.com
gru 03 2015
0

Templated control - gdy ważna jest wydajność

Ostatnio przy jednym z projektów służbowych, musiałem zrobić layout, który obejmowałby zagnieżdżanie kontrolek ListView. Poziomy scroll przesuwał elementy głównej listy, natomiast w każdym takim obiekcie, znajdował się panel zawierający pewne określone dane + kolejny ListView przesuwany już wertykalnie. Nie byłoby w tym nic nadzwyczajnego, gdyby nie fakt, że wewnętrzne kontrolki ListView, zawierały w sobie listę elementów na których znaleźć można było kontrolki użytkownika. Szybko okazało się, że to był błąd, bo w praktyce takie kontrolki w dużej ilości są strasznie nie wydajne, nawet przy zastosowanej wirtualizacji.

Na szczęście w porę przypomniałem sobie o innym rodzaju kontrolek, który pozwolił mi zaoszczędzić nawet kilkadziesiąt megabajtów pamięci i znacznie przyspieszył ładowanie widoku:-)

User control vs Templated control

Podstawowa różnica między tymi dwoma rodzajami kontrolek, jest dość prosta. Pierwsza z nich, wykorzystuje designer i standardowy code-behind. Mamy w tym przypadku dość dużą swobodę w tworzeniu rozbudowanych układów elementów. Druga z nich rozszerza klasę Control (bądź też inne popularne typy: Button, TextBox itp.) i skupia się na kodzie napisanym w C# (wykorzystanie metod OnApplyTemplate itp.). W tym drugim przypadku możemy również skorzystać z kodu XAML, który w tym przypadku umieszczany jest w pliku Generic.xaml w folderze Themes.

Z mojej perspektywy największą różnicą jest fakt, że w przypadku wykorzystaniu drugiego rodzaju kontrolek, template w XAMLu zaczytywany jest tylko jeden raz, a nie x razy jak w przypadku user controls. Daje to niesamowitego kopa w sytuacji gdy na stronie mamy np. kilkaset, lub nawet kilka tysięcy instancji określonej kontrolki. Oczywiście różnic jest znacznie więcej i dobrze opisuje je ten wątek na stackoverflow.

Przykładowa kontrolka

Stworzenie bazowej kontrolki jest banalnie proste. Wystarczy otworzyć dialog dodawania nowego elementu i na liście dostępnych elementów odnaleźć pozycję Templated control, a następnie wybrać nazwę dla nowej kontrolki.

Visual Studio utworzy nowy plik *.cs, w którym będziemy pisać nasz kod:

public sealed class MyControl : Control
{
    public MyControl()
    {
        this.DefaultStyleKey = typeof(MyControl);
    }
}

Oraz automatycznie doda przypisany wyżej w konstruktorze template. Wszystkie template'y umieszczane są tak jak wspomniałem w pliku Generic.xaml w folderze Themes. Poniżej zawartość przykładowego pliku wygenerowanego dla mnie przez VS:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App5">

    <Style TargetType="local:MyControl" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:MyControl">
                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Tak przygotowana kontrolka jest już zasadniczo gotowa do użycia i możemy ją też wklepać na naszej stronie:

<Page
    x:Class="App5.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App5"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid x:Name="LayoutRoot">
        <local:MyControl />
    </Grid>
</Page>

Oczywiście template control w takiej postaci nie robi praktycznie nic, ta kontrolka po prostu jest;-) Nie ma jednak problemu by do kontrolki dorzucić jakikolwiek kod, czy też nawet DependencyProperties (o DependencyProperty pisałem m.in: tutaj, a także tutaj i tutaj, dlatego też nie będę szerzej rozwijał wątku).

Chciałbym jednak zwrócić uwagę na inną rzecz, a mianowicie na metodę OnApplyTemplate, która jest wywoływana przy renderowaniu kontrolki i pozwala wpływać na layout z poziomu kodu C#. Jak to działa w praktyce? Właściwie to bardzo prosto. Poniżej przeprowadziłem modyfikacje pozwalające na prezentację tej metody w praktyce:

<Style TargetType="local:MyControl" >
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:MyControl">
                <TextBlock x:Name="MyControlTextBlock" Margin="0,0,0,19"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Uprościłem cały kod i wrzuciłem do środka tylko jeden, nazwany TextBlock. Modyfikacji dokonałem również w kodzie C#:

public sealed class MyControl : Control
{
    public MyControl()
    {
        this.DefaultStyleKey = typeof(MyControl);
    }

    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        var tb = this.GetTemplateChild("MyControlTextBlock") as TextBlock;
        if(tb != null)
        {
            tb.Text = Guid.NewGuid().ToString();
        }
    }
}

Przeciążyłem w tym przypadku metodę OnApplyTemplate, gdzie odnajduję TextBlock i nadaję mu losowego Guida. Metoda ta jest wywoływana tuż przed zdarzeniem wyświetlenia kontrolki na ekranie lub w przypadku ręcznego wywołania metody ApplyTemplate.

W kodzie naszej strony możemy również wymienić Grida na StackPanel i wrzucić kilka instancji naszej kontrolki:

<StackPanel x:Name="LayoutRoot" Margin="19">
    <local:MyControl />
    <local:MyControl />
    <local:MyControl />
    <local:MyControl />
    <local:MyControl />
</StackPanel>

Każda instancja wyświetli innego Guida:-)

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus