09 września 2011

WPF DataGrid w służbie metadanych

DataGrid i ja


Z programowaniem okienek w Windows rozstałem się na drugim roku studiów. Moje życie zawodowe tak się ułożyło, że nigdy później nie tknąłem Win32 API, MFC, WinForms,a tym bardziej WPF'a. W noworocznym postanowieniu ustaliłem, że spróbuję opanować chociaż podstawy Windows Presentation Foundation. W ramach realizacji postanowień, moja uwaga skupiła się na problemie prezentacji danych w postaci tabelarycznej w WPF.

W .NET Framework 3.5 do dyspozycji programisty WPF był WPF Toolkit, w którego skład wchodził DataGrid. W .NET Framework 4.0 kontrolka DataGrid wróciła z wygnania na codeplex.com i została włączona do Windows Presentation Foundation.

Ku mojemy zaskoczeniu DataGrid wykazał się dosyć rozbudowaną funkcjonalnością. Scenariusz, który chciałem zrealizować można zoobrazować w następujący sposób. Mamy listę piesków (dogs) i kotków (cats), wyświetlając listy tych zwierzaków nie chciałem deklaratywnie definiować każdego atrybutu w WPF'owym XAML'u. Chciałem wypracować bardziej dynamiczne podejście (cały kod projektu jest dostępny tutaj). Klasy kotków i piesków wyglądają jak poniżej.



public abstract class AnimalBase
{
    public string NickName { get; set; }

    public DateTime DateOfBirth { get; set; }
}

public class Dog : AnimalBase
{
    public string FavouriteFood { get; set; }

    public string BarkingSong { get; set; }
}

public class Cat : AnimalBase
{
    public string HasStripes { get; set; }

    public string MiaowTune { get; set; }
}


Dodajmy klasę AnimalPlanet która wygeneruje nam trochę testowych danych, a przy okazji przyda się do zademonstrowania deklaratywnego tworzenia instancji obiektów w XAML'u.

<usercontrol.resources>
   <wpfdatagrid:animalplanet x:key="Animals"></wpfdatagrid:animalplanet>
</usercontrol.resources>

public class AnimalPlanet
{
    private List<Cat> _cats;
    private List<Dog> _dogs;

    public AnimalPlanet()
    {
        _cats = new List<Cat>();
        _dogs = new List<Dog>();

        _cats.Add(new Cat
        {
            NickName = "Dachowiec", 
            DateOfBirth = DateTime.Now.AddMonths(-13).Date, 
            HasStripes = true,  
            MiaowTune = "DODA, Nie daj sie"
        });
        _cats.Add(new Cat 
        { 
            NickName = "Szczesciarz", 
            DateOfBirth = DateTime.Now.AddYears(-5).Date, 
            HasStripes = false, 
            MiaowTune = "DODA, Katharsis" 
        });
        
        _dogs.Add(new Dog 
        { 
            NickName = "Burek", 
            DateOfBirth = DateTime.Now.AddYears(-7).Date, 
            FavouriteFood = "Burito", 
            BarkingSong = "Feel, Jak aniola glos" 
        });
    }


    public List<Cat> Cats { get { return _cats; } }

    public List<Dog> Dogs { get { return _dogs; } }
}

Wersja deklaratywna


Pierwsza wersja deklaratywna listy kotków i piesków powoduje, że musimy deklarować nagłówki, powtarzać deklaracje kolumn itd. Oczywiście posługiwanie XAML'em jest preferowaną metodą w budowaniu WPF'owego UI, aczkolwiek ja chciałem rozwiązać ten w sumie prosty problem w trochę inny sposób.

DeclarativeDataGrid.xaml

<UserControl x:Class="WPFDataGrid.DeclarativeDataGrid"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:Animals="clr-namespace:WPFDataGrid.Domain.Animals">
    
    <UserControl.Resources>
        <Animals:AnimalPlanet x:Key="Animals"></Animals:AnimalPlanet>
    </UserControl.Resources>

    <Grid DataContext="{StaticResource ResourceKey=Animals}">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="30" />
            <RowDefinition />
        </Grid.RowDefinitions>
        
        <StackPanel Grid.Row="0">
            <TextBlock FontWeight="Bold">Dogs</TextBlock>
            <DataGrid ItemsSource="{Binding Dogs}" AutoGenerateColumns="False" HorizontalAlignment="Left" CanUserAddRows="False">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Nickname" Binding="{Binding NickName}" />
                    <DataGridTextColumn Header="Date of birth" Binding="{Binding DateOfBirth}" />
                    <DataGridTextColumn Header="Favourite Food" Binding="{Binding FavouriteFood}" />
                    <DataGridTextColumn Header="Barking Song" Binding="{Binding BarkingSong}" />
                </DataGrid.Columns>
            </DataGrid>
        </StackPanel>

        <StackPanel Grid.Row="2">
            <TextBlock FontWeight="Bold">Cats</TextBlock>
            <DataGrid ItemsSource="{Binding Cats}" AutoGenerateColumns="False" HorizontalAlignment="Left" CanUserAddRows="False">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Nickname" Binding="{Binding NickName}" />
                    <DataGridTextColumn Header="Date of birth" Binding="{Binding DateOfBirth}" />
                    <DataGridCheckBoxColumn Header="Has Stripes" IsThreeState="True" Binding="{Binding HasStripes}" />
                    <DataGridTextColumn Header="Miaow Tune" Binding="{Binding MiaowTune}" />
                </DataGrid.Columns>
            </DataGrid>
        </StackPanel>
    </Grid>
</UserControl>


Atrybuty na ratunek


W rozwiązaniu deklaratywnym każda kolumna DataGrid musiała mieć odpowiednik w XAML'u. Jeżeli mamy takich tabelek 10 to praktycznym rozwiązaniem może być generowanie kolumn tabeli na podstawie metadanych obiektów. Do definiowania metadanych użyłem atrybutów .NET. Atrybut VisibleItem dostarcza informacji o tym, które z właściwości (property) danej klasy należy wyświetlać w DataGrid. DisplayName definiuje nagłówek kolumny wyświetlającej dane. Poniżej definicja klasy VisibleItemAttribute, które określa, że ten atrybut może być stosowany do właściwości klas i dla danej właściwości może występować jednokrotnie.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class VisibleItemAttribute : Attribute
{
    public VisibleItemAttribute(string displayName)
    {
        DisplayName = displayName;
    }

    public string DisplayName { get; set; }
}

Po wprowadzeniu atrybutu VisibleItem klasa AnimalBase wygląda następująco.

public abstract class AnimalBase
{
    [VisibleItem("Nickname")]
    public string NickName { get; set; }

    [VisibleItem("Date Of Birth")]
    public DateTime DateOfBirth { get; set; }
}

XAML nowej wersji DataGrida został znacząco uproszczony, aczkolwiek code-behind klasy znacząco sie rozrósł, gdyż cała logika odczytywania metadanych obiektów została tam zaimplementowana.

MetadataGrid.xaml

<UserControl x:Class="WPFDataGrid.MetadataGrid"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition />
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0">
            <TextBlock FontWeight="Bold" Text="{Binding Title}" />
            <DataGrid Name="dgMetadata" AutoGenerateColumns="False" HorizontalAlignment="Left" CanUserAddRows="False" />
        </StackPanel>
    </Grid>
</UserControl>


MetadataGrid.xaml.cs

Metoda BuildColumnsFromMetadata generuje kolumny DataGrid'a w zależności od obecności atrybutu VisibleItem oraz typu danej właściwości obiektu.

public partial class MetadataGrid : UserControl
{
    public string Title { get; set; }

    private IList _itemsSource;

    public IList ItemsSource
    {
        get { return _itemsSource; }
        set
        {
            _itemsSource = value;
            BuildColumnsFromMetadata(_itemsSource);
            dgMetadata.ItemsSource = _itemsSource;
        }
    }

    public MetadataGrid()
    {
        InitializeComponent();
        DataContext = this;
    }

    private void BuildColumnsFromMetadata(IList data)
    {
        dgMetadata.Columns.Clear();
        if (data.Count > 0)
        {
            object firstElement = data[0];

            PropertyInfo[] properties = firstElement.GetType().GetProperties();
            foreach (PropertyInfo property in properties)
            {
                object[] visibleAttributes = property.GetCustomAttributes(typeof(VisibleItemAttribute), false);
                if (visibleAttributes.Length > 0)
                {
                    VisibleItemAttribute itemAttribute = (VisibleItemAttribute)visibleAttributes[0];
                    if (property.PropertyType.Name.Equals(typeof(bool).Name))
                    {
                        var checkBoxColumn = new DataGridCheckBoxColumn();
                        checkBoxColumn.Binding = new Binding(property.Name);
                        checkBoxColumn.Header = itemAttribute.DisplayName;
                        checkBoxColumn.IsThreeState = true;
                        dgMetadata.Columns.Add(checkBoxColumn);
                    }
                    else
                    {
                        var textColumn = new DataGridTextColumn();
                        textColumn.Binding = new Binding(property.Name);
                        textColumn.Header = itemAttribute.DisplayName;
                        dgMetadata.Columns.Add(textColumn);
                    }
                }
            }
        }
    }
}

Dodatkowo można byłoby dodać atrybuty grupowania, sortowania oraz rozszerzyć VisibleItem o właściwość kolejności kolumn i zaimplementować ich obsługę.

Hope this helps.

1 komentarz:

Uwaga: tylko uczestnik tego bloga może przesyłać komentarze.