Automaticky generované GUI pomocí reflexe - Automaticky generované GUI - Přehled objektů

Seznam článků

Automaticky generované GUI - Přehled objektů

Se znalostmi shrnutými v předchozích tabulkách, dokonce jen s některými z nich, by pak neměl být problém vytvořit jednotný kód, který vygeneruje přehled objektů. Pokud jde o jednoduchý jednosloupcový přehled typu ListView, stačí u třídy, jejíž instance má přehled zobrazovat, přepsat metodu ToString (metoda předka Object) tak, aby zobrazovala vhodný název objektu, například tak, jak ukazuje následující kód. 

public override string ToString() => $"{Prijmeni} {Jmeno} ({DatumNarozeni:yyyy})";  // "Novák Alois (1979)"

Pak už stačí jen tomuto ListView do vlastnosti ItemsSource přiřadit načtený seznam objektů (např. List<Osoba>) a základní seznam (bez šablony pro sofistikovanější strukturované zobrazení jednotlivých položek) je hotov (viz následující obrázek s náhodně vygenerovanými údaji).

Přehled záznamů v ListView

Výhodou komponenty ListView je jednak integrovaný vertikální posuvník a také podpora označování řádků tohoto přehledu, přičemž obvyklé je reagovat na výběr záznamu přepnutím se na stránku s detailním výpisem tohoto záznamu s možností editace jeho hodnot. Druhou možností je vlastní tlačítko pro zobrazení detailu záznamu, avšak i to je vázáno na řádek vybraný v přehledu. ListView také umožňuje definovat vlastní šablonu pro zobrazení jednotlivých řádků, takže nemusí nutně obsahovat pouze jednu textovou hodnotu, ale lze k nim třeba přidat obrázek, rozdělit je do více řádků apod. Nevýhodu ovšem je, že při pokusu o rozmísťování jednotlivých hodnot každého řádku do sloupců tak, aby vznikla obdoba tabulky (mřížky/gridu), bude každý řádek samostatnou tabulkou a případné přizpůsobení šířky sloupce některé delší hodnotě se na ostatních řádcích tato změna neprojeví a v přehledu tak vznikají "nevzhledné zuby".

Pokud bychom chtěli objekty zobrazit v tabulkovém přehledu, v XF tedy v mřížce (Grid), pak je důležité, kolik jich zhruba bude. Je-li počet únosný (např. do cca 50ti), lze jej zobrazit všechny v rámci jednoho přehledu uvnitř ScrollView. Mělo-li by však položek (řádků) být více, a to nejen v okamžiku publikování aplikace, ale i třeba po několika letech jejího používání a plnění daty, bude nezbytné ke stávajícímu příkladu přidat podporu stránkování, tzn. načítání vždy jen určité podčásti dat (např. po 20ti záznamech) a v tabulce zobrazovat pouze ty.

Jelikož v tomto případě budeme často přidávat prvky do gridu na příslušné souřadnice řádku a sloupce, vytvoříme si nejprve následující metodu, která nám tuto činnost usnadní. Ta umožní do Gridu přímo vložit prvek na konkrétní souřadnice (řádek a sloupec mřížky), v případě potřeby jej roztáhnout přes více buněk (-span), nastavit prvku konkrétní horizontální odsazení (margin) a vertikálně jej vycentrovat. To vše v rámci jedné metody, která navíc vkládaný prvek vrací a umožňuje na něj větvit další případné metody. Přitom defaultní hodnoty vstupních parametrů metody jsou voleny tak, aby je v drtivé většině případů vůbec nebylo nutné definovat. 

public static class LayoutUtils
{
    public static View AddChild(this Grid grd, View child, int row, int col, int rowSpan = 1, int colSpan = 1,
        double? horizontalMargin = 10, bool centerVertically = true)
    {
        Grid.SetRow(child, row);               // Řadek
        Grid.SetColumn(child, col);            // Sloupec
        Grid.SetRowSpan(child, rowSpan);       // Roztažení přes více řádků
        Grid.SetColumnSpan(child, colSpan);    // Roztažení přes více sloupců
        if (centerVertically)                  // Vertikální vycentrování (pokud není zadáno, nechá jej takové, jaké je)
            child.VerticalOptions = LayoutOptions.Center;   
        if (horizontalMargin != null)          // Horizontální odsazení (zleva a zprava), vertikální nechá se stávajícími hodnotami
            child.Margin = new Thickness((double)horizontalMargin, child.Margin.Top, (double)horizontalMargin, child.Margin.Bottom); 
        grd.Children.Add(child);               // Přidání prvku do gridu
        return child;                          // Vrácení prvku (rpo případnou další práci s ním, zvláště bude-li vytvořen přímo v rámci této metody)
    }
}

Vytvoření tabulky (Grid) i s obsahem lze pojmout několika způsoby. Jednou z hlavních možností by asi byla statická metoda, která by celý grid z vytvořila na základě vstupního seznamu objektů a vrátila jej jako výstupní hodnotu. Její hlavička by pak mohla vypadat třeba následovně. 

public static Grid CreateGridWithData<T>(IEnumerable<T> data) { ... }

Grid, který by metoda vrátila, bychom pak přes C# kód vložili na místo, kam potřebujeme. Pokud bychom však raději Grid chtěli umístit již v rámci XAML návrhu designu, bude výhodnější jej připravit jako komponentu. Jelikož jej celý generujeme dynamicky, není potřeba této komponentě vytvářet XAML definici a můžeme ji zapsat pouze v C# tak, jak ukazuje následující kód třídy DataGrid

public class DataGrid : Grid
{
    public delegate void OnButtonClickDelegate(object clickedItem); // Delegát (typ události) ohlašující požadavek na editaci záznamu
    public event OnButtonClickDelegate OnButtonClick;               // Událost, která oznámí požadavek na editaci objektu (clickedItem)

    public void SetData<T>(IEnumerable<T> data)
    {
        // Vymazat stávající obsah gridu
        Children.Clear();            // Vymaže komponenty vložené v gridu
        ColumnDefinitions.Clear();   // Zruší všechny stávající sloupce
        RowDefinitions.Clear();      // Zruší všechny stávající řádky
        if (data == null || data.Count() == 0) return;         // Nejsou-li zadaná vstupní data, není co generovat

        // Typ položek
        var t = data.FirstOrDefault().GetType().GetTypeInfo(); // TypeInfo tříd objektů zjistíme z první položky
        var props = t.GetProperties();                         // Seznam vlastností třídy

        // Vytvoření sloupců a jejich záhlaví
        RowDefinitions.Add(new RowDefinition());               // První řádek se záhlavím
        for (int j = 0; j < props.Length; j++)                 // Pro všechny vlastnosti třídy...
        {
            ColumnDefinitions.Add(new ColumnDefinition());     // Vytvoření sloupce (jen v prvním řádku)
            // Pokud je sloupec určen pro numerické hodnoty, jeho šířka bude určena nejširší hodnotou, nikoli podílem na šířce okna jako u textů
            if (props[j].PropertyType == typeof(int))       
                ColumnDefinitions.Last().Width = new GridLength(1, GridUnitType.Auto);
            // Přidání popisku (názvu vlastnosti) v záhlaví (v prvním řádku) sloupce
            this.AddChild(new Label() { Text = props[j].Name, FontAttributes = FontAttributes.Bold }, 0, j);
        }
        // Sloupec pro tlačítka umožňující editaci záznamu
        ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
        this.AddChild(new Label() { Text = "Detail", FontAttributes = FontAttributes.Bold }, 0, ColumnDefinitions.Count - 1);

        // Řádky s daty
        foreach (var item in data)
        {
            RowDefinitions.Add(new RowDefinition()); // Přidání řádku pro záznam
            for (int j = 0; j < props.Length; j++)   // Pro všechny vlastnosti třídy...
            {
                var val = props[j].GetValue(item);   // Získání hodnoty vlastnosti z objektu
                var lbl = new Label();               // Label, který bude hodnotu zobrazovat
                // Naformátování hodoty na základě jejího datového typu
                if (props[j].PropertyType == typeof(DateTime)) // Datum
                    lbl.Text = (val as DateTime?)?.ToString("dd.MM.yyyy");
                else // Vše ostatní lze (zatím) přeložit na text klasickým způsobem
                    lbl.Text = val?.ToString();
                // Zarovnání čísel doprava
                if (props[j].PropertyType == typeof(int))
                    lbl.HorizontalOptions = LayoutOptions.End;
                // Přidání do gridu na správné místo
                this.AddChild(lbl, RowDefinitions.Count - 1, j);
            }
            // Tlačítko pro editaci do posledního sloupce
            var btnEdit = new Button() { Text = "Edit", BindingContext = item };
            btnEdit.Clicked += btnEdit_Clicked;
            this.AddChild(btnEdit, RowDefinitions.Count - 1, ColumnDefinitions.Count - 1);
        }
    }

    private void btnEdit_Clicked(object sender, EventArgs e)
    {
        OnButtonClick?.Invoke(((Button)sender).BindingContext); // Oznámení, že došlo ke kliknutí na tlačítko Edit u záznamu
    }

    // Vytvoření nového Gridu i s obsahem na základě seznamu objektů
    public static DataGrid CreateGridWithData<T>(IEnumerable<T> data)
    {
        var grid = new DataGrid();
        grid.SetData(data);
        return grid;
    }
}

Tato naše nová třída DataGrid dědí ze třídy Grid a získává tak veškeré její vlastnosti, metody a události, které již nemusíme nijak řešit. Přidává ji navíc událost OnButtonClick, která bude oznamovat, že uživatel klikl na (nebo "tapnul" či jinak stiskl) tlačítko "Edit", které bude vygenerováno u každého řádku záznamu. Toto lze samozřejmě řešit mnoha způsoby i bez tlačítka, například zachytáváním kliknutí na celý řádek každého záznamu (přes TapGestureRecognizer), implementovat lze v podstatě i podporu výběru (označení) záznamu, podobně jako to umí ListView (např. změnou barvy nějakého panelu, třeba BoxView, v pozadí celého řádku, jež by právě mohl i zachytávat kliknutí).

Další přidanou položkou ve třídě DataGrid je metoda SetData. Tu je třeba volat pro vygenerování (popř. přegenerování) Gridu (sloupců, řádků, hodnot), pokud se data jakkoli změnila (např. po editaci, výmazu či přidání záznamu), podobně jako při nastavení vlastnosti ItemsSource u ListView. Stejný princip by se dal implementovat i zde, přidáním této vlastnosti (ItemsSource) do naší třídy, kde by se v jejím set-kódu volala právě tato metoda (SetData(value);). Dalším rozšířením by také mohla být podpora seznamů typu ObservableCollection, u něhož by přehled dat na změny v seznamu reagoval automaticky překreslením těch, kterých se změna týkala. To však (tentokrát) není cílem tohoto tutoriálu.

Třída DataGrid obsahuje dokonce i dříve zmíněnou statickou metodu CreateGridWithData, která v případě, že by měl být grid generován dynamicky a ne přes XAML, ušetří nějaké řádky a vygeneruje, naplní a vrátí sám sebe. Pokud však preferujete umístění tabulky v XAML definici, pak by to mohlo vypadat například takto. 

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Reflexe.PrehledDat"
             xmlns:local="clr-namespace:Reflexe"
             Title="Osoby">
    <ContentPage.Content>
        <ScrollView>
            <Grid>
                <local:DataGrid x:Name="gOsoby" Margin="20" />
            </Grid>
        </ScrollView>
    </ContentPage.Content>
</ContentPage>

Možný výsledek (opět s náhodně vygenerovanými údaji) po nastavení obsahu DataGridu metodou SetData ukazuje následující obrázek.

reflexe gridPřehled záznamů v Gridu 

Z výsledku je patrné několik detailů, které by ještě stály za doladění. Například popisky v záhlaví sloupců jsou pouhými názvy vlastností, takže neobsahují diakritiku, mezery mezi slovy atd. Toto se dá doladit s využitím atributů a resources, čemuž se budeme věnovat později. Tímtéž postupem by šlo řešit více individuálně nastavené šířky sloupců, vynechání některých vlastností z tabulkového přehledu (vidět by byly až v detailu záznamu), změnu pořadí sloupců atd. Nejdříve se ale podíváme na to, jak vygenerovat formulář pro editaci jednotlivých záznamů. 

 

on 27 květen 2018