Monthly Archives: Czerwiec 2012

Łączenie kolekcji obiektów przy użyciu LINQ

2012-06-28

W dzisiejszym wpisie zajmę się tematem łączenia kolekcji obiektów przy użyciu mechanizmów dostępnych w LINQ. Pokażę przykłady zastosowań metod Concat, Union, Intersect, Except, Zip oraz klauzuli Join (zarówno dla złączeń wewnętrznych jak i zewnętrznych). Dla każdego przykładu złączenia kolekcji przedstawię analogiczny sposób łączenia zbiorów danych w języku SQL. Na początku konieczne jest przygotowanie tabel i danych testowych w języku SQL oraz ich odpowiedników w postaci klas i kolekcji obiektów w C#.

Poniższy skrypt przygotowuje trzy tabele: #F1Teams (wybrane zespoły Formuły 1), #DriversChampions (kierowcy startujący w sezonie 2012, posiadający tytuł mistrza świata), #DriversWinners (kierowcy, którzy wygrali przynajmniej jeden wyścig w sezonie 2012):

create table #F1Teams
(
    Name varchar(50) not null,
    Country varchar(50) not null
)

create table #DriversChampions
(
    FirstName varchar(50) not null,
    LastName varchar(50) not null,
    Team varchar(50) not null
)

create table #DriversWinners
(
    FirstName varchar(50) not null,
    LastName varchar(50) not null,
    Team varchar(50) not null
)

insert into #F1Teams (Name, Country)
values
    ('Ferrari', 'Włochy'),
    ('McLaren', 'Wielka Brytania'),
    ('Mercedes', 'Niemcy'),
    ('Red Bull', 'Austria'),
    ('Williams', 'Wielka Brytania')

insert into #DriversChampions (FirstName, LastName, Team)
values
    ('Fernando', 'Alonso', 'Ferrari'),
    ('Jenson', 'Button', 'McLaren'),
    ('Lewis', 'Hamilton', 'McLaren'),
    ('Kimi', 'Raikkonen', 'Lotus'),
    ('Michael', 'Schumacher', 'Mercedes'),
    ('Sebastian', 'Vettel', 'Red Bull')

insert into #DriversWinners (FirstName, LastName, Team)
values
    ('Fernando', 'Alonso', 'Ferrari'),
    ('Jenson', 'Button', 'McLaren'),
    ('Lewis', 'Hamilton', 'McLaren'),
    ('Pastor', 'Maldonado', 'Williams'),
    ('Nico', 'Rosberg', 'Mercedes'),
    ('Sebastian', 'Vettel', 'Red Bull'),
    ('Mark', 'Webber', 'Red Bull')

select * from #F1Teams
select * from #DriversChampions
select * from #DriversWinners

Poniższy kod tworzy klasy F1Team i Driver oraz kolekcje obiektów odpowiadające utworzonym wcześniej tabelom w bazie danych:

class F1Team
{
    public string Name { get; set; }
    public string Country { get; set; }

    public F1Team(string name, string country)
    {
        Name = name;
        Country = country;
    }
}

class Driver
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Team { get; set; }

    public Driver(string firstName, string lastName, string team)
    {
        FirstName = firstName;
        LastName = lastName;
        Team = team;
    }
}

List<F1Team> F1Teams;
List<Driver> DriversChampions;
List<Driver> DriversWinners;

private void PrepareData()
{
    F1Teams = new List<F1Team>
    {
        new F1Team("Ferrari", "Włochy"),
        new F1Team("McLaren", "Wielka Brytania"),
        new F1Team("Mercedes", "Niemcy"),
        new F1Team("Red Bull", "Austria"),
        new F1Team("Williams", "Wielka Brytania")
    };

    DriversChampions = new List<Driver>
    {
        new Driver("Fernando", "Alonso", "Ferrari"),
        new Driver("Jenson", "Button", "McLaren"),
        new Driver("Lewis", "Hamilton", "McLaren"),
        new Driver("Kimi", "Raikkonen", "Lotus"),
        new Driver("Michael", "Schumacher", "Mercedes"),
        new Driver("Sebastian", "Vettel", "Red Bull")
    };

    DriversWinners = new List<Driver>
    {
        new Driver("Fernando", "Alonso", "Ferrari"),
        new Driver("Jenson", "Button", "McLaren"),
        new Driver("Lewis", "Hamilton", "McLaren"),
        new Driver("Pastor", "Maldonado", "Williams"),
        new Driver("Nico", "Rosberg", "Mercedes"),
        new Driver("Sebastian", "Vettel", "Red Bull"),
        new Driver("Mark", "Webber", "Red Bull")
    };
}

class DriverComparer : IEqualityComparer<Driver>
{
    public bool Equals(Driver d1, Driver d2)
    {
        if (Object.ReferenceEquals(d1, d1))
            return true;

        if ((d1 == null) || (d2 == null))
            return false;

        return d1.FirstName == d2.FirstName &&
            d1.LastName == d2.LastName &&
            d1.Team == d2.Team;
    }

    public int GetHashCode(Driver d)
    {
        if (d == null)
            return 0;

        int hashFirstName = (d.FirstName == null) ? 0 : d.FirstName.GetHashCode();
        int hashLastName = (d.LastName == null) ? 0 : d.LastName.GetHashCode();
        int hashTeam = (d.Team == null) ? 0 : d.Team.GetHashCode();

        return hashFirstName ^ hashLastName ^ hashTeam;
    }
}

Dodatkowo utworzona została klasa DriverComparer implementująca interfejs IEqualityComparer<T>. Zawiera ona metody służące do porównania ze sobą dwóch obiektów danego typu (w tym przypadku klasy Driver). Obiekt klasy DriverComparer wykorzystywany będzie przy metodach Union, Intersect oraz Except w celu stwierdzenia, czy wybrane elementy dwóch kolekcji są sobie równe.

Concat (Union all)

Połączenie danych z tabel #DriversChampions i #DriversWinners:

select FirstName, LastName, Team from #DriversChampions
union all
select FirstName, LastName, Team from #DriversWinners

Połączenie elementów z kolekcji DriversChampions i DriversWinners:

var ChampionsConcatWinners = DriversChampions.Concat(DriversWinners);

Union

Połączenie danych z tabel #DriversChampions i #DriversWinners z eliminacją duplikatów (wyniki zawierają tylko unikalne dane):

select FirstName, LastName, Team from #DriversChampions
union
select FirstName, LastName, Team from #DriversWinners

Połączenie elementów z kolekcji DriversChampions i DriversWinners z eliminacją duplikatów (wynikowa kolekcja zawiera tylko unikalne dane, w celu porównania obiektów z dwóch kolekcji wykorzystany został obiekt klasy DriverComparer):

var ChampionsUnionWinners = DriversChampions.Union(DriversWinners, new DriverComparer());

lub:

var ChampionsUnionWinners =
    DriversChampions.Concat(DriversWinners).Distinct(new DriverComparer());

Intersect

Zwrócenie wspólnych danych z tabel #DriversChampions i #DriversWinners:

select FirstName, LastName, Team from #DriversChampions
intersect
select FirstName, LastName, Team from #DriversWinners

Zwrócenie wspólnych elementów z kolekcji DriversChampions i DriversWinners:

var ChampionsIntersectWinners =
    DriversChampions.Intersect(DriversWinners, new DriverComparer());

Except

Zwrócenie danych z tabeli #DriversChampions nie występujących w tabeli #DriversWinners:

select FirstName, LastName, Team from #DriversChampions
except
select FirstName, LastName, Team from #DriversWinners

Zwrócenie elementów z kolekcji DriversChampions nie występujących w kolekcji DriversWinners:

var ChampionsExceptWinners =
    DriversChampions.Except(DriversWinners, new DriverComparer());

Join (Inner join)

Złączenie tabel #DriversChampions i #F1Teams:

select d.FirstName, d.LastName, d.Team, t.Country
from #DriversChampions d
    inner join #F1Teams t on t.Name = d.Team

Złączenie kolekcji DriversChampions i F1Teams:

var DriversTeams = from d in DriversChampions
                   join t in F1Teams on d.Team equals t.Name
                   select new
                   {
                       FirstName = d.FirstName,
                       LastName = d.LastName,
                       Team = d.Team,
                       Country = t.Country
                   };

Join (Left join)

Złączenie zewnętrzne tabel #DriversChampions i #F1Teams:

select d.FirstName, d.LastName, d.Team, t.Country
from #DriversChampions d
    left join #F1Teams t on t.Name = d.Team

Złączenie zewnętrzne kolekcji DriversChampions i F1Teams:

var DriversTeams = from d in DriversChampions
                   join t in F1Teams on d.Team equals t.Name
                   into TempF1Teams
                   from tt in TempF1Teams.DefaultIfEmpty()
                   select new
                   {
                       FirstName = d.FirstName,
                       LastName = d.LastName,
                       Team = d.Team,
                       Country = (tt == null) ? String.Empty : tt.Country
                   };

Inny sposób, z wykorzystaniem domyślnego elementu kolekcji:

F1Team defaultF1Team = new F1Team(String.Empty, String.Empty);

var DriversTeams = from d in DriversChampions
                   join t in F1Teams on d.Team equals t.Name
                   into TempF1Teams
                   from tt in TempF1Teams.DefaultIfEmpty(defaultF1Team)
                   select new
                   {
                       FirstName = d.FirstName,
                       LastName = d.LastName,
                       Team = d.Team,
                       Country = tt.Country
                   };

Zip

Ta metoda nie ma swojego odpowiednika w języku SQL, przy jej użyciu możemy łączyć elementy dwóch kolekcji znajdujące się na tych samych pozycjach (mające ten sam indeks). Jeżeli łączone kolekcje posiadają różną ilość elementów, w wyniku otrzymamy ich tyle ile miała mniejsza z kolekcji. Poniższy przykład prezentuje dwie kolekcje zawierające informacje o czterech okrążeniach pokonanych przez bolid F1. Kolekcja lapTimes przechowuje czasy poszczególnych okrążeń, natomiast kolekcja lapTyres rodzaj opon w jakie wyposażony był bolid. Za pomocą metody Zip informacje te zostały połączone i umieszczone w kolekcji lapInfo. Dodatkowo wykorzystana została przeciążona wersja metody Select pozwalająca na numerowanie poszczególnych elementów kolekcji:

List<TimeSpan> lapTimes = new List<TimeSpan>
{
    new TimeSpan(0, 1, 32, 15, 230),
    new TimeSpan(0, 1, 31, 43, 120),
    new TimeSpan(0, 1, 28, 34, 660),
    new TimeSpan(0, 1, 55, 23, 730)
};

List<string> lapTyres = new List<string>
{
    "Medium",
    "Medium",
    "Soft",
    "Intermediate"
};

var lapInfo = lapTimes.Zip(lapTyres, (time, tyre) =>
    new { LapTime = time, LapTyre = tyre });

var lapInfoFormated = lapInfo.Select((i, n) =>
    string.Format("Okrążenie nr: {0}, Czas: {1}, Opony: {2}", n + 1, i.LapTime, i.LapTyre));

Oto zawartość kolekcji lapInfoFormated:

Okrążenie nr: 1, Czas: 01:32:15.2300000, Opony: Medium
Okrążenie nr: 2, Czas: 01:31:43.1200000, Opony: Medium
Okrążenie nr: 3, Czas: 01:28:34.6600000, Opony: Soft
Okrążenie nr: 4, Czas: 01:55:23.7300000, Opony: Intermediate

Na zakończenie jedna uwaga. Nie ma potrzeby przekazywania obiektu IEqualityComparer<T> do wybranych metod w przypadku łączenia ze sobą kolekcji zawierających referencje do tych samych obiektów.

Nowości w SQL Server 2012 – funkcje analityczne

2012-06-23

W najnowszej wersji SQL Server język T-SQL wzbogacony został o kilka nowych funkcji analitycznych. Są to tzw. funkcje okienkowe operujące na podzbiorach (oknach danych tworzonych klauzulą OVER) głównego zbioru zwróconego przez zapytanie. Zanim przejdę do omówienia wspomnianych funkcji pokażę co zmieniło się w samym poleceniu OVER. Polecenie umożliwiało tworzenie podzbiorów danych poprzez umieszczane w nim instrukcje PARTITION BY i ORDER BY. Obecnie na potrzeby danej funkcji każdy taki podzbiór możemy podzielić na jeszcze mniejsze okna danych. Służy do tego instrukcja ROWS zawężająca dany podzbiór do określonych rekordów. Po słowie ROWS definiujemy zakres rekordów jaki wejdzie w skład danego okna, odbywa się to poprzez polecenia:

  • CURRENT ROW – bieżący rekord;
  • PRECEDING – liczba rekordów poprzedzających bieżący (UNBOUNDED PRECEDING oznacza początek podzbioru);
  • FOLLOWING – liczba kolejnych rekordów po bieżącym (UNBOUNDED FOLLOWING oznacza koniec podzbioru);

Polecenia 0 PRECEDING oraz 0 FOLLOWING są jednoznaczne z CURRENT ROW.
Zamiast ROWS operującego na fizycznych pozycjach względem bieżącego rekordu, możemy użyć polecenia RANGE, które odnosi się do rekordów poprzez ich wartości. I tak np. CURRENT ROW w przypadku RANGE to wszystkie rekordy posiadające w polach z klauzuli ORDER BY taką samą wartość jak rekord bieżący.

Aby pokazać działanie omawianych mechanizmów przygotowałem tabelę tymczasową zawierającą średnie miesięczne ceny paliw w okresie czerwiec – wrzesień 2011 roku (a wówczas narzekaliśmy, że jest drogo ;)):

create table #CenyPaliw
(
    Rok int,
    Miesiac int,
    Paliwo varchar(10),
    Cena money
)

insert into #CenyPaliw(Rok, Miesiac, Paliwo, Cena)
values
    (2011, 6, 'PB95', 5.11), (2011, 6, 'ON', 4.95), (2011, 6, 'LPG', 2.49),
    (2011, 7, 'PB95', 5.13), (2011, 7, 'ON', 4.94), (2011, 7, 'LPG', 2.48),
    (2011, 8, 'PB95', 5.21), (2011, 8, 'ON', 5.05), (2011, 8, 'LPG', 2.47),
    (2011, 9, 'PB95', 5.10), (2011, 9, 'ON', 5.08), (2011, 9, 'LPG', 2.54)

select * from #CenyPaliw

Poniższe zapytanie pokazuje zastosowanie nowych możliwości polecenia OVER:

select Paliwo, Rok, Miesiac, Cena,
    max(cena) over(partition by paliwo) as [Max],
    max(cena) over(partition by paliwo order by rok, miesiac
        rows between unbounded preceding and current row) as [MaxDoTeraz],
    max(cena) over(partition by paliwo order by rok, miesiac
        rows between 2 preceding and 0 following) as [MaxOd3Mcy],
    min(cena) over(partition by paliwo order by rok, miesiac
        rows between current row and 2 following) as [MinDo3Mcy],
    min(cena) over(partition by paliwo order by rok, miesiac
        rows between current row and unbounded following) as [MinOdTeraz]
from #CenyPaliw
order by Paliwo, Rok, Miesiac

W poszczególnych kolumnach znajdują się następujące informacje w odniesieniu do każdego rodzaju paliwa:

  • Max – maksymalna cena paliwa w całym badanym okresie;
  • MaxDoTeraz – maksymalna cena paliwa do danego miesiąca (rekordu);
  • MaxOd3Mcy – maksymalna cena paliwa z ostatnich trzech miesięcy (względem danego rekordu);
  • MinDo3Mcy – minimalna cena paliwa w trzech kolejnych miesiącach (względem danego rekordu);
  • MinOdTeraz – minimalna cena paliwa od danego miesiąca do końca okresu;

Po zapoznaniu się z rozszerzonymi możliwościami klauzuli OVER czas przejść do prezentacji nowych funkcji analitycznych. Są to funkcje: LAG, LEAD, FIRST_VALUE, LAST_VALUE, PERCENT_RANK, CUME_DIST, PERCENTILE_CONT oraz PERCENTILE_DISC. Ich działanie w odniesieniu do ceny każdego rodzaju paliwa pokazuje poniższe zapytanie:

select Paliwo, Rok, Miesiac, Cena,
    lag(cena, 1, 0) over(partition by paliwo order by rok, miesiac) as [Poprzednia],
    lead(cena, 1, 0) over(partition by paliwo order by rok, miesiac) as [Nastepna],
    first_value(cena) over(partition by paliwo order by rok, miesiac
        rows unbounded preceding) as [Pierwsza],
    last_value(cena) over(partition by paliwo order by rok, miesiac
        rows between current row and unbounded following) as [Ostatnia],
    percent_rank() over(partition by paliwo order by rok, miesiac) as [Rank1],
    cume_dist() over(partition by paliwo order by rok, miesiac) as [Rank2],
    percentile_cont(0.5) within group (order by cena)
        over (partition by paliwo) as [Med1],
    percentile_disc(0.5) within group (order by cena)
        over (partition by paliwo) as [Med2]
from #CenyPaliw
order by Paliwo, Rok, Miesiac

Poszczególne funkcje oraz kolumny, w których zostały użyte:

  • LAG [Poprzednia] – domyślnie funkcja zwraca wartość określonego pola z poprzedniego rekordu podzbioru, jeżeli jest to pierwszy rekord zwraca NULL, opcjonalnie można podać większy offset i wartość domyślną (w tym przypadku odpowiednio 1 i 0);
  • LEAD [Nastepna] – domyślnie funkcja zwraca wartość określonego pola z następnego rekordu podzbioru, jeżeli jest to ostatni rekord zwraca NULL, opcjonalnie można podać większy offset i wartość domyślną (w tym przypadku odpowiednio 1 i 0);
  • FIRST_VALUE [Pierwsza] – funkcja zwraca wartość pola z pierwszego rekordu w określonym oknie danych;
  • LAST_VALUE [Ostatnia] – funkcja zwraca wartość pola z ostatniego rekordu w określonym oknie danych;
  • PERCENT_RANK [Rank1] – ranking rekordu w danym podzbiorze obliczany wg wzoru: (RN – 1) / (RC – 1), gdzie RN to kolejny numer rekordu (funkcja Rank), RC to liczba rekordów w danym podzbiorze;
  • CUME_DIST [Rank2] – ranking rekordu w danym podzbiorze obliczany wg wzoru: RS / RC, gdzie RS to liczba rekordów o wartości mniejszej bądź równej od bieżącego (dotyczy klauzuli ORDER BY), RC to liczba rekordów w danym podzbiorze;
  • PERCENTILE_CONT [Med1] – funkcja zwraca percentyl dla przekazanego procentu (0.0 – 1.0). Jest to wielkość, poniżej której padają wartości zadanego procentu rekordów z danego podzbioru, np. percentyl 50% to mediana. Do określenia kolumny, której wartości będą analizowane służy dodatkowa klauzula WITHIN GROUP. Zwrócony wynik nie musi pokrywać się z jakąkolwiek wartością w danym podzbiorze;
  • PERCENTILE_DISC [Med2] – funkcja działa analogicznie do powyższej, z tą różnicą, iż zwrócony wynik zawsze pokrywa się z wartością występującą w danym podzbiorze;

Jak widać nowe funkcje znacznie upraszczają proces analizy, eliminując konieczność tworzenia szeregu skomplikowanych podzapytań w celu uzyskania interesujących nas danych.

Więcej informacji o nowościach w SQL Server 2012: MSDN

WPF – dynamiczne tworzenie i wczytywanie kodu XAML

2012-06-18

W dzisiejszym wpisie pokażę w jaki sposób dla danego obiektu WPF wygenerować kod XAML oraz jak taki kod wczytać dynamicznie podczas działania programu. W tym celu użyję klas XamlWriter i XamlReader. Klasa XamlWriter poprzez metodę Save umożliwia wygenerowanie (oraz ewentualne zapisanie do pliku) kodu XAML dla przekazanego w parametrze obiektu. Z kolei klasa XamlReader przy wykorzystaniu metody Load lub Parse pozwala na wczytanie kodu XAML i utworzenie na jego podstawie odpowiedniego obiektu (zwracany typ to object).

Aby zaprezentować działanie wspomnianych klas utworzyłem metody CreateXamlFile oraz LoadXamlFile. Pierwsza z nich tworzy trzy kontrolki (Label, TextBox, Button), następnie pojemnik StackPanel, po czym dodaje do niego utworzone wcześniej elementy. Na koniec poprzez klasę XamlWriter generowany jest kod XAML dla obiektu StackPanel i zapisywany do pliku:

private void CreateXamlFile()
{
    Label label = new Label
    {
        Name = "label1",
        Content = "Label",
        Height = 28,
        Width = 80,
        HorizontalContentAlignment = System.Windows.HorizontalAlignment.Center,
        FontSize = 16,
        Foreground = new SolidColorBrush(Colors.RoyalBlue),
        FontWeight = FontWeights.Bold,
        Margin = new Thickness(0, 20, 0, 0)
    };

    TextBox textBox = new TextBox
    {
        Name = "textBox1",
        Text = "TextBox",
        Height= 23,
        Width = 120,
        Background = new SolidColorBrush(Colors.GreenYellow),
        BorderBrush = new SolidColorBrush(Colors.Black),
        Margin = new Thickness(0, 20, 0, 0)
    };

    Button button = new Button
    {
        Name = "button1",
        Content = "Button",
        Height = 48,
        Width = 95,
        FontSize = 16,
        Foreground = new SolidColorBrush(Colors.White),
        Background = new SolidColorBrush(Colors.RoyalBlue),
        Margin = new Thickness(0, 20, 0, 0)
    };

    StackPanel stackPanel = new StackPanel
    {
        Name = "stackPanel1",
        Background = new SolidColorBrush(Colors.AliceBlue),
        Orientation = Orientation.Vertical
    };

    stackPanel.Children.Add(label);
    stackPanel.Children.Add(textBox);
    stackPanel.Children.Add(button);

    using (FileStream fs = new FileStream(@"D:\StackPanel.xaml", FileMode.Create))
    {
        System.Windows.Markup.XamlWriter.Save(stackPanel, fs);
    }

    /*
    Wygenerowanie kodu XAML i zwrócenie go w postaci string:
    string xamlCode = System.Windows.Markup.XamlWriter.Save(stackPanel);
    */
}

Jak widać w zakomentowanym fragmencie kodu, można skorzystać z przeciążonej metody Save w celu zwrócenia wygenerowanego kodu XAML jako string.

Zawartość utworzonego pliku StackPanel.xaml:

<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Name="stackPanel1" Orientation="Vertical" Background="#FFF0F8FF">
    <Label Name="label1" Foreground="#FF4169E1" FontSize="16" FontWeight="Bold"
        HorizontalContentAlignment="Center" Width="80" Height="28" Margin="0,20,0,0">
        Label
    </Label>
    <TextBox Name="textBox1" BorderBrush="#FF000000" Background="#FFADFF2F"
        Width="120" Height="23" Margin="0,20,0,0">
        TextBox
    </TextBox>
    <Button Name="button1" Background="#FF4169E1" Foreground="#FFFFFFFF"
        FontSize="16" Width="95" Height="48" Margin="0,20,0,0">
        Button
    </Button>
</StackPanel>

Metoda LoadXamlFile przy wykorzystaniu klasy XamlReader odczytuje zapisany wcześniej kod XAML i zwraca zdefiniowany w nim obiekt StackPanel:

private object LoadXamlFile()
{
    object xamlObject;

    using (FileStream fs = new FileStream(@"D:\StackPanel.xaml", FileMode.Open))
    {
        xamlObject = System.Windows.Markup.XamlReader.Load(fs);
    }

    /*
    Utworzenie obiektu na podstawie kodu XAML przekazanego jako string:
    xamlObject = System.Windows.Markup.XamlReader.Parse(xamlCode);
    */

    return xamlObject;
}

W zakomentowanym kodzie znajduje się przykład użycia metody Parse, która zwraca obiekt utworzony na podstawie kodu XAML przekazanego w parametrze jako string.

W jaki sposób utworzyć obiekty na podstawie kodu XAML oraz wykorzystać je w programie podczas jego działania? Pokażę to na przykładzie wczytywania zawartości okna. W tym celu do projektu WPF dodałem puste okno (nazwałem je DynamicWindow), którego zawartość będzie ładowana dynamicznie z utworzonego wcześniej pliku StackPanel.xaml.

DynamicWindow.xaml:

<Window x:Class="XAMLDynamic.DynamicWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DynamicWindow" Height="230" Width="255"
        WindowStartupLocation="CenterScreen">
</Window>

DynamicWindow.xaml.cs:

/// <summary>
/// Interaction logic for DynamicWindow.xaml
/// </summary>
public partial class DynamicWindow : Window
{
    public DynamicWindow(object content = null)
    {
        InitializeComponent();
        LoadContent(content);
    }

    private void LoadContent(object content)
    {
        if (content != null)
        {
            DependencyObject dependencyObject = content as DependencyObject;
            this.Content = dependencyObject;
            Button button = LogicalTreeHelper.FindLogicalNode(
                dependencyObject, "button1") as Button;
            if (button != null)
                button.Click += new RoutedEventHandler(button1_Click);
        }
    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Click!!!");
    }
}

W Code-Behind utworzyłem metodę LoadContent, a do konstruktora dodałem parametr content oraz wywołanie tej metody. Jeżeli do konstruktora przekażemy określony obiekt, metoda LoadContent ustawi go jako zawartość tworzonego okna (this.Content). Dodatkowo w metodzie LoadContent pokazałem w jaki sposób obsłużyć zdarzenie w dynamicznie załadowanym obiekcie. Można to zrobić poprzez klasę LogicalTreeHelper i jej metodę FindLogicalNode, która w drzewie elementów przekazanego obiektu szuka elementu o podanej nazwie (w tym przypadku button1). Po odnalezieniu przycisku o nazwie button1 do jego zdarzenia Click podpinana jest metoda button1_Click.

Jeżeli utworzymy okno bez przekazania obiektu w parametrze konstruktora, zobaczymy pustą zawartość:

DynamicWindow dynamicWindow = new DynamicWindow();
dynamicWindow.ShowDialog();

Jeżeli przekażemy obiekt utworzony na podstawie kodu XAML wczytanego z pliku StackPanel.xaml (metoda LoadXamlFile), zobaczymy oczekiwaną zawartość okna i działające zdarzenie Click:

object content = LoadXamlFile();
DynamicWindow dynamicWindow = new DynamicWindow(content);
dynamicWindow.ShowDialog();

W zaprezentowanym przykładzie dynamicznie wczytany obiekt został wykorzystany jako zawartość okna. Oczywiście nic nie stoi na przeszkodzie aby wykorzystać go w inny sposób, np. dodać do kolekcji elementów istniejącego obiektu (Children.Add). Można również do pliku XAML zapisać całe okno, a następnie przy odczycie rzutować uzyskany obiekt na typ Window i otwierać bezpośrednio poprzez metodę Show lub ShowDialog.

Więcej informacji o opisywanych klasach można znaleźć na MSDN:  XAML Overview (WPF)XamlReaderXamlWriter

Data Tools i Power Tools – dodatki do Visual Studio 2010

2012-06-14

Wraz z najnowszą wersją SQL Server firma Microsoft udostępniła darmowy dodatek do Visual Studio 2010 pod nazwą SQL Server Data Tools (SSDT). Z kolei ten produkt niedawno doczekał się rozszerzenia: SQL Server Data Tools Power Tools (SSDT Power Tools). Głównym zadaniem wymienionych narzędzi jest umożliwienie wygodnej pracy z bazami danych SQL Server z poziomu Visual Studio. Dzięki nim nie ma już konieczności korzystania jednocześnie z Visual Studio (kod aplikacji) i Management Studio (kod bazy danych), ponieważ ten pierwszy otrzymał wszystkie funkcjonalności niezbędne do operowania na bazach danych. Instalacja obydwu narzędzi nie stanowi problemu i zajmuje kilkanaście minut (najpierw instalujemy SSDT, później SSDT Power Tools). Po jej zakończeniu możemy zweryfikować czy produkty zostały prawidłowo zainstalowane:

Help -> About Microsoft Visual Studio:

Tools -> Extension Manager:

Po zainstalowaniu Data Tools w głównym menu pojawi się pozycja SQL, natomiast do menu View dojdzie nowe okno: SQL Server Object Explorer. I właśnie z poziomu tego okna mamy dostęp do nowych funkcjonalności. Możemy łączyć się z serwerami SQL, zarządzać bazami danych, tworzyć/modyfikować/usuwać obiekty, edytować dane czy po prostu pisać zapytania. Większość rzeczy, które do tej pory robiliśmy w Management Studio możemy teraz wykonywać bezpośrednio w Visual Studio:

W łatwy sposób możemy debugować procedury składowane lub funkcje. Wystarczy kliknąć prawym klawiszem na interesujący nas obiekt i wybrać Debug Procedure lub Debug Function. Przydatną funkcją jest także Schema Compare dostępna w menu SQL lub z menu kontekstowego bazy danych (po zainstalowaniu Power Tools). Pozwala ona na porównanie schematów wybranych baz danych i wskazanie różnic między nimi. Po zakończeniu analizy możemy uruchomić proces ujednolicenia wybranej bazy lub wygenerować skrypt realizujący to zadanie.

Co z kolei udostępnia nam rozszerzenie Power Tools? Po pierwsze daje możliwość generowania skryptów dla obiektów bazy danych (opcja Script As z menu kontekstowego obiektu). Po drugie (i jest to główna funkcjonalność) w oknie SQL Server Object Explorer pojawia się gałąź Projects, która w przypadku projektu bazodanowego udostępnia widok bazy danych wygenerowany na podstawie elementów (skryptów) znajdujących się w projekcie. Widok ten pozwala na łatwe odnajdywanie skryptu zawierającego definicje wybranego obiektu, wystarczy z menu kontekstowego wybrać Select in Solution Explorer. Ponadto na poszczególnych obiektach mamy dostęp do opcji refactoringu.

Na koniec zostawiłem chyba najciekawszą funkcjonalność, a mianowicie pracę z bazą danych bez dostępu do serwera (offline). W tym celu w oknie SQL Server Object Explorer klikamy prawym klawiszem na bazę danych i wybieramy Create New Project. Po ustawieniu kilku opcji w oknie Import Database utworzony zostanie projekt bazodanowy będący odzwierciedleniem wybranej bazy danych. Od tego momentu możemy pracować bez dostępu do źródłowego serwera, wszystkie zmiany jakich dokonujemy zapisywane są w projekcie, a tworzone obiekty powstają w lokalnej bazie danych (LocalDB) dostępnej w gałęzi SQL Server.

Po uzyskaniu dostępu do serwera możemy zaktualizować wybraną bazę danych na podstawie naszego projektu. W tym celu z menu kontekstowego projektu (okno Solution Explorer) wybieramy opcję Publish, w której po skonfigurowaniu połączenia możemy uruchomić proces aktualizacji lub jedynie wygenerować odpowiedni skrypt. Przed wykonaniem tej czynności warto sprawdzić konfigurację projektu, a dokładniej opcję Target platform, która pozwala na określenie docelowej wersji bazy danych (2005, 2008, 2012, Azure).

Na zakończenie kilka uwag. Aby zainstalować opisywane dodatki Visual Studio 2010 musi posiadać SP1. W przypadku aktualizacji SSDT Power Tools wcześniej musimy odinstalować poprzednią wersję (menu Tools -> Extension Manager). Oprócz opisanych funkcji, dodatek Data Tools rozszerza Visual Studio 2010 o możliwość tworzenia projektów bazodanowych na platformę SQL Server 2012 (New Project -> Other Languages -> SQL Server). W Visual Studio 2012 dodatek ten jest już zintegrowany ze środowiskiem.

Linki:

Nowości w SQL Server 2012 – odczyt struktury zwracanych danych

2012-06-10

W SQL Server 2012 pojawiło się kilka obiektów systemowych pozwalających na uzyskanie szczegółowych informacji odnośnie struktury danych zwracanych przez określone zapytanie, bez konieczności jego uruchamiania. Do tego celu służy procedura sp_describe_first_result_set oraz funkcje sys.dm_exec_describe_first_result_set i sys.dm_exec_describe_first_result_set_for_object. W wyniku ich działania otrzymujemy zestaw danych opisujących pierwszy zbiór zwrócony przez dane zapytanie lub obiekt (procedura, funkcja, widok, itp.). Jeżeli dane polecenie nie zwraca zbioru danych otrzymamy pusty wynik.

W celu prezentacji działania powyższych obiektów stworzyłem tabelę i procedurę składowaną:

create table Test
(
    Id int not null primary key,
    Name varchar(50)
)

create procedure pTest
as
    select * from Test

sp_describe_first_result_set

Procedura sp_describe_first_result_set przyjmuje trzy parametry, z czego dwa ostatnie są opcjonalne. Jako pierwszy parametr przekazywane jest polecenie SQL, dla którego chcemy uzyskać informacje na temat struktury danych wynikowych. Drugi parametr pozwala na określenie argumentów polecenia SQL. Trzeci z parametrów przyjmuje wartości 0, 1 lub 2 i służy do wyboru rodzaju prezentowanych informacji. W wyniku działania procedury otrzymujemy szczegółowy opis struktury pierwszego zbioru danych zwróconych przez podane polecenie SQL. W poszczególnych wierszach znajdują się opisy kolumn zwracanego zbioru. Najważniejsze informacje to:

  • is_hidden – czy kolumna jest ukryta
  • name – nazwa kolumny
  • column_ordinal – pozycja kolumny
  • is_nullable – czy kolumna zezwala na wartości NULL
  • system_type_id – identyfikator typu danych
  • system_type_name – nazwa typu danych
  • max_length – maksymalny rozmiar danych
  • collation_name – collation danych tekstowych w kolumnie

Pełna lista wraz z opisami znajduje się na MSDN.

Przykład użycia:

exec sp_describe_first_result_set N'select * from Test'

Zwrócenie informacji tylko o widocznych kolumnach (domyślnie):

exec sp_describe_first_result_set N'select Name from Test', null, 0

Zwrócenie informacji także o kolumnach niewidocznych (uwzględniona została kolumna Id będąca kluczem głównym tabeli, is_hidden = 1):

exec sp_describe_first_result_set N'select Name from Test', null, 1


W przypadku gdy określone polecenie zwraca więcej niż jeden zbiór danych, udostępnione zostaną informacje tylko o pierwszym z nich:

exec sp_describe_first_result_set N'select Id from Test; select Id, Name from Test'


Jeżeli polecenie w zależności od warunku logicznego zwraca zbiory danych o różnej strukturze wygenerowany zostanie błąd:

exec sp_describe_first_result_set N'if (1=1) select Id from Test else select Name from Test'

sys.dm_exec_describe_first_result_set

Funkcja dm_exec_describe_first_result_set działa w identyczny sposób jak omówiona wcześniej procedura, jednak w jej przypadku podanie wszystkich trzech parametrów jest obowiązkowe:

select * from sys.dm_exec_describe_first_result_set(N'select * from Test', null, 0)

select name, system_type_name
from sys.dm_exec_describe_first_result_set(N'pTest', null, 0)
order by column_ordinal

Istnieje jeszcze jedna różnica w stosunku do procedury sp_describe_first_result_set. Jeżeli przekazane w parametrze polecenie będzie błędne procedura zgłosi wyjątek. W przypadku funkcji wynik zostanie zwrócony, a informacje o błędach zostaną umieszczone w odpowiednich kolumnach:

  • error_number – numer błędu
  • error_severity – poziom błędu
  • error_state – stan błędu
  • error_message – komunikat błędu
  • error_type – typ błędu
  • error_type_desc – opis typu błędu
--Odwołanie do nieistniejącego obiektu Test1
select * from sys.dm_exec_describe_first_result_set(N'select * from Test1', null, 0)

sys.dm_exec_describe_first_result_set_for_object

Funkcja dm_exec_describe_first_result_set_for_object działa analogicznie do powyższej, jednak jako pierwszy parametr przyjmuje ID obiektu procedury składowanej lub triggera (w przypadku innych obiektów zgłoszony zostanie błąd). W jej przypadku nie podaje się parametru z listą argumentów:

select * from sys.dm_exec_describe_first_result_set_for_object(Object_Id('pTest'), 0)

Więcej informacji na ten temat można znaleźć na MSDN: sp_describe_first_result_set
sys.dm_exec_describe_first_result_set
, sys.dm_exec_describe_first_result_set_for_object

Śledzenie i kontrola zmian danych w obiekcie DataTable

2012-06-03

W dzisiejszym wpisie zajmę się tematem śledzenia zmian i kontroli danych w obiekcie DataTable. Jest to możliwe dzięki temu, że wiersze tabeli będące obiektami typu DataRow przechowują informacje o swoich wersjach. Obiekty DataRow posiadają także szereg metod związanych z modyfikacją danych, a co za tym idzie ze zmianą ich wersji. Do pobrania określonej wersji obiektu DataRow służy enum DataRowVersion posiadający następujące elementy:

  • Original – oryginalna wersja danych (po ostatnim wykonaniu metody AcceptChanges)
  • Current – aktualna wersja danych
  • Proposed – proponowana wersja danych
  • Default – domyślna wersja danych

Do sprawdzenia czy dana wersja istnieje służy właściwość DataRow.HasVersion przyjmująca jako parametr jeden z elementów powyższego enum-a, w wyniku zwracając wartość bool. W celu pobrania wartości kolumny z określonej wersji rekordu musimy po nazwie lub identyfikatorze kolumny podać wersję jaka nas interesuje.

Zmiany poszczególnych wersji wiersza tabeli mają miejsce podczas wywoływania następujących metod obiektu DataRow:

  • BeginEdit() – rozpoczęcie edycji, wersja Current zawiera aktualną wersję danych, Proposed zawiera zmodyfikowane dane
  • EndEdit() – zakończenie edycji, wersja Proposed staje się wersją Current
  • CancelEdit() – anulowanie edycji, wersja Proposed jest usuwana
  • AcceptChanges() – zatwierdzenie wszystkich zmian od ostatniego wywołania AcceptChanges, wersja Original przyjmuje wartości z wersji Current
  • RejectChanges() – wycofanie wszystkich zmian od ostatniego wywołania AcceptChanges (wycofywane są także zmiany mimo wywołania EndEdit)

Metody AcceptChanges i RejectChanges dostępne są także na obiekcie DataTable, działają one analogicznie do powyższych ale swoim zasięgiem obejmują wszystkie wiersze tabeli.

W jakim celu należy używać powyższych metod skoro modyfikacja danych w DataTable możliwa jest bez nich? Załóżmy, że zmieniamy dane w wybranym wierszu obiektu DataTable z przypisanym zdarzeniem RowChanging, w którym kontrolujemy poprawność wprowadzanych danych. Jeżeli nie użyjemy tych metod zdarzenie będzie wywoływane przy zmianie wartości w każdej kolumnie, co negatywnie odbije się na wydajności. Najlepiej gdyby walidacja wiersza uruchomiana została dopiero w momencie zakończenia modyfikacji wszystkich kolumn. Dlatego właśnie warto stosować opisane metody. Po wywołaniu DataRow.BeginEdit wstrzymywane jest wyzwalanie zdarzeń RowChanging i RowChanged do momentu wywołania DataRow.EndEdit. We wspomnianych zdarzeniach warto na początku sprawdzić stan wiersza (e.Row.RowState) lub rodzaj akcji (e.Action) ponieważ są one wywoływane zarówno podczas EndEdit jak i AcceptChanges oraz RejectChanges. Action przyjmuje wartość Add, Change lub Delete w przypadku EndEdit, Commit w przypadku AcceptChanges oraz Rollback w przypadku RejectChanges.

Poniższy kod pokazuje przykładowe zastosowanie omawianych mechanizmów. Tworzony jest obiekt DataTable, dodawane są do niego dwa rekordy, następnie pierwszy z nich jest modyfikowany. Ponadto w zdarzeniu RowChanging sprawdzane jest czy podczas modyfikacji danych kolumna Id nie przyjmuje wartości mniejszej niż miała wcześniej oraz czy kolumna Name nie jest pusta:

private void DataTableExample()
{
    DataTable dataTable = new DataTable();
    dataTable.Columns.Add("Id", Type.GetType("System.Int32"));
    dataTable.Columns.Add("Name", Type.GetType("System.String"));

    dataTable.Rows.Add(1, "Name_1");
    dataTable.Rows.Add(2, "Name_2");
    dataTable.AcceptChanges();

    dataTable.RowChanging += new DataRowChangeEventHandler(dataTable_RowChanging);

    DataRow dataRow = dataTable.Rows[0];

    dataRow.BeginEdit();
    /*
    dataRow["Id", DataRowVersion.Original]: 1
    dataRow["Id", DataRowVersion.Current]: 1
    */
    dataRow["Id"] = 3;
    dataRow["Name"] = "Name_3";
    /*
    dataRow["Id", DataRowVersion.Original]: 1
    dataRow["Id", DataRowVersion.Current]: 1
    dataRow["Id", DataRowVersion.Proposed]: 3
    */
    try
    {
        dataRow.EndEdit();
        /*
        dataRow["Id", DataRowVersion.Original]: 1
        dataRow["Id", DataRowVersion.Current]: 3
        */
    }
    catch (Exception ex)
    {
        dataRow.CancelEdit();
    }
    if (dataRow.RowState == DataRowState.Modified)
        dataRow.AcceptChanges();
    /*
    dataRow["Id", DataRowVersion.Original]: 3
    dataRow["Id", DataRowVersion.Current]: 3
    */
}

private void dataTable_RowChanging(object sender, DataRowChangeEventArgs e)
{
    if (e.Action == DataRowAction.Change)
    {
        int currentId = (int)e.Row["Id", DataRowVersion.Current];
        int proposedId = currentId;
        if (e.Row.HasVersion(DataRowVersion.Proposed))
            proposedId = (int)e.Row["Id", DataRowVersion.Proposed];
        //W tym miejscu wersja Proposed jest domyślna więc można ją pobrać bezpośrednio, powyższa konstrukcja użyta została w celach prezentacji
        string proposedName = e.Row["Name"].ToString();

        if ((proposedId < currentId) || string.IsNullOrWhiteSpace(proposedName))
            throw new Exception("Nieprawidłowe dane!");
    }
}

Więcej informacji na ten temat można znaleźć na MSDN: DataRowDataRowVersion