Monthly Archives: Lipiec 2012

SSIS Script Component Transformation – własna transformacja danych

2012-07-27

W poprzednim temacie opisałem użycie komponentu Script jako własnego źródła danych w Integration Services. Dzisiaj przedstawię kolejne zastosowanie Script Component, a mianowicie tworzenie własnej transformacji danych. Konfiguracja komponentu po umieszczeniu go w elemencie Data Flow przebiega następująco:

1. Skrypt ustawiamy jako transformację danych:

2. W sekcji Script poprzez właściwości ReadOnlyVariables i ReadWriteVariables udostępniamy dla skryptu zmienne.

3. W sekcji Input Columns wybieramy dostępne kolumny wejściowe oraz określamy czy wartości w nich podlegają modyfikacji (Usage Type):

4. W sekcji Inputs and Outputs definiujemy wyjściowe zbiory danych. Najważniejsze ustawienia każdego zbioru to SynchronousInputID oraz ExclusionGroup. Opcja SynchronousInputID określa, czy dany zbiór wyjściowy jest zsynchronizowany ze wskazanym zbiorem wejściowym. Jeżeli w opcji tej wybierzemy zbiór wejściowy, dane wyjście automatycznie otrzyma taką samą strukturę kolumn (istnieje możliwość dodania nowych kolumn, wówczas podczas przetwarzania rekordów należy uzupełniać dla nich dane). Jeżeli nie wskażemy zbioru wejściowego musimy ręcznie zdefiniować kolumny, a w samym skrypcie zadbać o wypełnienie zbioru wyjściowego danymi. Z kolei opcja ExclusionGroup odnosi się do zbiorów wyjściowych zsynchronizowanych z danym wejściem. Jeżeli jej wartość będzie wynosiła 0 na dane wyjście automatycznie przesyłane będą wszystkie rekordy z wybranego wejścia (po ewentualnych modyfikacjach i uzupełnieniu danych w dodatkowych kolumnach). Jeżeli jej wartość będzie większa od 0 wybrane rekordy muszą być kierowane do danego wyjścia za pomocą metody DirectRowTo<nazwa_wyjścia>(). Na poniższym przykładzie zdefiniowane zostały trzy zbiory wyjściowe:

  • MyOutput1 (zsynchronizowany z wejściem, ExclusionGroup = 0) – wszystkie rekordy wejściowe po przetworzeniu są automatycznie przesyłane na wyjście;
  • MyOutput2 (zsynchronizowany z wejściem, ExclusionGroup > 0) – wybrane rekordy wejściowe po przetworzeniu są kierowane do wyjścia przy użyciu metody Row.DirectRowToMyOutput2();
  • MyOutput3 (brak synchronizacji z wejściem) – konieczne jest zdefiniowanie kolumn zbioru wyjściowego, rekordy są w nim umieszczane poprzez metodę MyOutput3Buffer.AddRow();

5. W sekcji Connection Managers udostępniamy dla skryptu istniejące połączenia (oczywiście jeżeli chcemy z nich skorzystać).

Po zakończeniu konfiguracji możemy przejść do kodu C# (sekcja Script -> Edit Script). Skrypt składa się z klasy ScriptMain, w której implementujemy wybrane metody:

  • AcquireConnections – metoda, w której możemy nawiązać połączenie z zewnętrznym źródłem (możemy zdefiniować nowe połączenie lub wykorzystać udostępnione, w drugim przypadku używamy obiektu Connections zawierającego listę połączeń oraz metody AcquireConnection udostępniającej konkretne połączenie);
  • PreExecute – metoda wykonywana jest przed rozpoczęciem procesu przetwarzania danych;
  • <nazwa_wejścia>_ProcessInput – metoda w pętli dla każdego rekordu w buforze wejściowym wywołuje metodę <nazwa_wejścia>_ProcessInputRow;
  • <nazwa_wejścia>_ProcessInputRow – metoda wykonywana dla każdego rekordu wejściowego, tutaj implementujemy logikę transformacji oraz przekierowujemy rekordy do zbiorów wyjściowych;
  • CreateNewOutputRows – metoda, w której możemy dodawać nowe rekordy do wyjściowych zbiorów danych (wywołanie metody AddRow na wyjściowym zbiorze dodaje do niego nowy rekord, dla którego następnie należy uzupełnić dane), do zbioru odwołujemy się poprzez <nazwa_wyjścia>Buffer;
  • PostExecute – metoda wykonywana po zakończeniu procesu przetwarzania danych;
  • ReleaseConnections – w tej metodzie należy zamknąć połączenia do zewnętrznych źródeł;

Wszystkie dostępne metody: ScriptComponent Class

Poniższy skrypt przetwarza rekordy ze zbioru wejściowego i umieszcza dane w trzech zbiorach wyjściowych zgodnie z ich konfiguracją:

using System;
using System.Data;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;

[Microsoft.SqlServer.Dts.Pipeline.SSISScriptComponentEntryPointAttribute]
public class ScriptMain : UserComponent
{
    public override void MyInput_ProcessInputRow(MyInputBuffer Row)
    {
        //MyOutput1
        Row.Name = Row.Name.ToLower();

        //MyOutput2
        if (Row.Id > 1)
            Row.DirectRowToMyOutput2();

        //MyOutput3
        MyOutput3Buffer.AddRow();
        MyOutput3Buffer.Id = Row.Id + 10;
        MyOutput3Buffer.Name = Row.Name.Substring(1, 1);
    }

    public override void CreateNewOutputRows()
    {
        MyOutput3Buffer.AddRow();
        MyOutput3Buffer.Id = 0;
        MyOutput3Buffer.Name = "New";
    }
}

Użycie przygotowanego komponentu:

W wyniku jego działania otrzymujemy oczekiwane dane (od góry: zbiór wejściowy, wyjście MyOutput1, wyjście MyOutput2, wyjście MyOutput3):

Więcej informacji na ten temat: MSDN1, MSDN2.

SSIS Script Component Source – własne źródło danych

2012-07-22

W dzisiejszym temacie omówię tworzenie własnego źródła danych w Integration Services za pomocą skryptu C#. Do tego celu służy komponent Script (Script Component). Po dodaniu go do elementu Data Flow, należy skonfigurować kilka opcji:

1. Skrypt ustawiamy jako źródło danych:

2. W sekcji Script poprzez właściwości ReadOnlyVariables i ReadWriteVariables udostępniamy dla skryptu zmienne:


3. W sekcji Inputs and Outputs określamy nazwę wyjściowego zbioru danych oraz definiujemy dla niego listę kolumn (możemy utworzyć więcej niż jeden zbiór):

4. W sekcji Connection Managers udostępniamy dla skryptu istniejące połączenia (oczywiście jeżeli chcemy z nich skorzystać):

Po skonfigurowaniu komponentu klikamy na Edit Script (sekcja Script) i przechodzimy do edycji kodu C#. Skrypt składa się z klasy ScriptMain, w której implementujemy określone metody w celu przygotowania i udostępnienia danych. Są to metody:

  • AcquireConnections – metoda, w której możemy nawiązać połączenie z zewnętrznym źródłem (możemy zdefiniować nowe połączenie lub wykorzystać udostępnione, w drugim przypadku używamy obiektu Connections zawierającego listę połączeń oraz metody AcquireConnection udostępniającej konkretne połączenie);
  • PreExecute – metoda wykonywana jest przed rozpoczęciem procesu przetwarzania danych, tutaj najlepiej przygotować dane, które mają być udostępnione;
  • CreateNewOutputRows – metoda tworząca wyjściowy zbiór danych (wywołanie metody AddRow na wyjściowym zbiorze dodaje do niego nowy rekord, dla którego następnie należy uzupełnić dane), do zbioru odwołujemy się poprzez <nazwa_zbioru>Buffer;
  • PostExecute – metoda wykonywana po przygotowaniu wynikowego zbioru danych, tutaj najlepiej zwalniać obiekty użyte do jego przygotowania;
  • ReleaseConnections – w tej metodzie należy zamknąć połączenia do zewnętrznych źródeł;

Wszystkie dostępne metody: ScriptComponent Class

Oto przykład prostego skryptu pobierającego i udostępniającego zbiór z bazy danych:

using System;
using System.Data;
using System.Data.SqlClient;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;

[Microsoft.SqlServer.Dts.Pipeline.SSISScriptComponentEntryPointAttribute]
public class ScriptMain : UserComponent
{
    IDTSConnectionManager100 connectionManager;
    SqlConnection sqlConnection;
    SqlDataReader dataReader;

    public override void AcquireConnections(object Transaction)
    {
        connectionManager = this.Connections.DBConnection;
        sqlConnection = (SqlConnection)connectionManager.AcquireConnection(null);
    }

    public override void PreExecute()
    {
        base.PreExecute();
        int id = Variables.Id;
        SqlCommand sqlCommand = new SqlCommand(
            "select Id, Name from Table_A where Id >= " + id.ToString(), sqlConnection);
        dataReader = sqlCommand.ExecuteReader();
    }

    public override void CreateNewOutputRows()
    {
        while (dataReader.Read())
        {
            OutputDataBuffer.AddRow();
            OutputDataBuffer.Id = Convert.ToInt32(dataReader["Id"]);
            OutputDataBuffer.Name = Convert.ToString(dataReader["Name"]);
        }
    }

    public override void PostExecute()
    {
        base.PostExecute();
        dataReader.Close();
    }

    public override void ReleaseConnections()
    {
        connectionManager.ReleaseConnection(sqlConnection);
    }

    /* This script component can fire events.
     *
     * Example of firing an error event:
     *  ComponentMetaData.FireError(10, "Process Values", "Bad value", "", 0, out cancel);
     *
     * Example of firing an information event:
     *  ComponentMetaData.FireInformation(10, "Process Values", "Start", "", 0, fireAgain);
     *
     * Example of firing a warning event:
     *  ComponentMetaData.FireWarning(10, "Process Values", "No rows were received", "", 0);
     */
}

W zakomentowanym fragmencie znajdują się przykłady wywoływania zdarzeń podczas działania skryptu.

Więcej informacji na ten temat: MSDN.

SSIS DataReader Destination – odczyt danych w aplikacji .NET

2012-07-13

W ostatnim wpisie pokazałem w jaki sposób wczytać i uruchomić paczkę Integration Services w aplikacji .NET. Dzisiaj rozszerzę ten temat, prezentując kod C# pozwalający na pobranie danych z paczki SSIS zawierającej komponent DataReader Destination. Komponent ten poprzez interfejs DataReader udostępnia aplikacjom zewnętrznym zestaw danych z elementu DataFlow. Przed próbą odczytu danych w aplikacji zewnętrznej konieczne jest odpowiednie skonfigurowanie obiektów paczki SSIS. W komponencie DataReader Destination oprócz zdefiniowania listy kolumn należy ustawić trzy podstawowe właściwości:

  • Name – nazwa, po której aplikacja zewnętrzna będzie się odwoływać do komponentu;
  • ReadTimeout – czas w milisekundach, przez jaki dane są udostępniane;
  • FailOnTimeout – zachowanie po upływie wyznaczonego czasu na odczyt, w przypadku False dane po prostu przestają być udostępniane, w przypadku True zgłaszany jest wyjątek: „The SSIS IDataReader is closed. The read timeout has expired”;

Drugą ważną rzeczą jest definicja zmiennych. Jeżeli wartość danej zmiennej ma być ustawiana z poziomu aplikacji zewnętrznej, zmienna taka musi znajdować się w przestrzeni DtsClient. Oto prosty obiekt DataFlow, z którego pobierane będą dane:

Teraz możemy przejść do kodu C# odczytującego dane z paczki SSIS. Na początku w projekcie należy dodać referencje do Microsoft.SqlServer.Dts.DtsClient.dll (domyślna lokalizacja pliku w przypadku SQL Server 2012 to C:\Program Files\Microsoft SQL Server\110\DTS\Binn). Wszystkie niezbędne klasy znajdują się w przestrzeni Microsoft.SqlServer.Dts.DtsClient. Pierwszym krokiem jest utworzenie obiektu DtsConnection oraz wskazanie we właściwości ConnectionString lokalizacji paczki SSIS. Następnie tworzony jest obiekt DtsCommand z właściwością CommandText zawierającą nazwę komponentu DataReader Destination, z którego pobierane będą dane. Kolejnym etapem jest utworzenie i skonfigurowanie obiektu DtsDataParameter odpowiedzialnego za przypisanie wartości do zmiennej Id. Ostatnim krokiem jest wywołanie metody DtsCommand.ExecuteReader, w wyniku której otrzymujemy obiekt IDataReader udostępniający dane z paczki SSIS. W poniższym przykładzie odczytane dane umieszczane są w obiekcie DataTable.

public void SSISDataReader()
{
    DtsConnection SSISConnection = new DtsConnection();
    SSISConnection.ConnectionString = @"/FILE ""D:\DataReader.dtsx""";
    SSISConnection.Open();

    DtsCommand SSISCommand = new DtsCommand(SSISConnection);
    SSISCommand.CommandText = "DataReaderDest";

    DtsDataParameter SSISParameter = new DtsDataParameter("Id", DbType.Int32);
    SSISParameter.Direction = ParameterDirection.Input;
    SSISCommand.Parameters.Add(SSISParameter);
    SSISParameter.Value = 1;

    IDataReader dataReader = SSISCommand.ExecuteReader(CommandBehavior.CloseConnection);

    DataTable dataTable = new DataTable();
    dataTable.Load(dataReader);

    SSISCommand.Dispose();
    dataReader.Close();
}

Jeżeli uruchamiana paczka posiada plik konfiguracyjny możemy wskazać do niego ścieżkę poprzez parametr /ConfigFile w DtsConnection.ConnectionString. Opis pozostałych opcji: dtexec Utility.

Więcej informacji na ten temat: MSDN.

Załadowanie i uruchomienie paczki SSIS z poziomu aplikacji .NET

2012-07-07

W dzisiejszym temacie przedstawię przykładowy kod C# umożliwiający załadowanie i uruchomienie paczki Integration Services. Na początku w projekcie konieczne jest dodanie referencji do Microsoft.SQLServer.ManagedDTS.dll. Wszystkie użyte klasy znajdują się w przestrzeni Microsoft.SqlServer.Dts.Runtime. Poniższy kod za pomocą obiektu klasy Application i metody LoadPackage ładuje paczkę z określonej lokalizacji, następnie dla wczytanej paczki (klasa Package) importowany jest plik konfiguracyjny (metoda ImportConfigurationFile) oraz ustawiana wartość zmiennej Id (właściwość Variables), na koniec paczka jest uruchamiana za pomocą metody Execute. W wyniku tej metody otrzymujemy obiekt DTSExecResult informujący o statusie zakończenia operacji.

public void ExecSSISPackage()
{
    Microsoft.SqlServer.Dts.Runtime.Application SSISApplication =
        new Microsoft.SqlServer.Dts.Runtime.Application();

    Package SSISPackage = SSISApplication.LoadPackage(@"D:\LoadData.dtsx", null);

    /*
    Załadowanie paczki z serwera:

    if (SSISApplication.ExistsOnSqlServer("packagePath", "serverName",
        "serverUserName", "serverPassword"))
        SSISPackage = SSISApplication.LoadFromSqlServer("packagePath",
            "serverName", "serverUserName", "serverPassword", null);
    */

    SSISPackage.ImportConfigurationFile(@"D:\LoadData.dtsConfig");
    SSISPackage.Variables["Id"].Value = 1;

    DTSExecResult ExecResult = SSISPackage.Execute();
}

W zakomentowanym fragmencie kodu znajduje się przykład załadowania paczki z serwera SQL.

Poniżej znajduje się kod rozszerzający wcześniejszy przykład o możliwość przechwytywania zdarzeń występujących podczas wykonywania paczki. Do tego celu należy stworzyć oddzielną klasę dziedziczącą po DefaultEvents. Klasa ta ma na celu obsłużenie każdego zdarzenia, które chcemy przechwycić. Utworzony obiekt tej klasy przekazywany jest do metody Execute wczytanej paczki.

class PackageEvents : DefaultEvents
{
    public override bool OnError(DtsObject source, int errorCode, string subComponent,
      string description, string helpFile, int helpContext, string idofInterfaceWithError)
    {
        MessageBox.Show("Wystąpił błąd w " + source + ", " +
            subComponent + ": " + description);
        return false;
    }
}

public void ExecSSISPackageWithEvents()
{
    Microsoft.SqlServer.Dts.Runtime.Application SSISApplication =
        new Microsoft.SqlServer.Dts.Runtime.Application();

    Package SSISPackage = SSISApplication.LoadPackage(@"D:\LoadData.dtsx", null);

    SSISPackage.ImportConfigurationFile(@"D:\LoadData.dtsConfig");
    SSISPackage.Variables["Id"].Value = 1;

    PackageEvents SSISEvents = new PackageEvents();

    DTSExecResult ExecResult = SSISPackage.Execute(null, null, SSISEvents, null, null);
}

Aby możliwe było skorzystanie z opisanych mechanizmów, na danym komputerze musi być zainstalowany Integration Services. Sposób na uruchomienie paczki SSIS z komputera nie posiadającego tego narzędzia opisany jest na MSDN.

Więcej informacji na ten temat: MSDN.

WPF – własny układ oraz wygląd elementów dzięki ItemsPanelTemplate i DataTemplate

2012-07-02

W WPF dostępne są klasy ItemsPanelTemplate i DataTemplate, dzięki którym możemy zdefiniować własny układ oraz wygląd elementów prezentowanych przez wybraną kontrolkę. W tym temacie pokażę zastosowanie wspomnianych klas na przykładzie obiektu ListView wyświetlającego elementy kolekcji. Klasa ItemsPanelTemplate definiuje panel odpowiedzialny za sposób ułożenia poszczególnych elementów. Każda kontrolka dziedzicząca po ItemsControl (taką kontrolką jest m.in. ListView) posiada właściwość ItemsPanel, do której możemy przypisać obiekt ItemsPanelTemplate w celu zastosowania określonego w nim układu. Poszczególne kontrolki dziedziczące po ItemsControl posiadają domyślne obiekty ItemsPanelTemplate, i tak np. dla ListBox jest to VirtualizingStackPanel, dla MenuItem jest to WrapPanel, a dla StatusBar DockPanel. Z kolei klasa DataTemplate pozwala na określenie sposobu prezentacji (wyglądu) danego elementu. Kontrolki ItemsControl posiadają właściwość ItemTemplate, do której możemy przypisać obiekt DataTemplate. Dzięki temu poszczególne elementy w danej kontrolce otrzymają wygląd zdefiniowany w tym obiekcie. W przypadku kontrolki ListView z widokiem ustawionym jako GridView możemy użyć obiektu DataTemplate do określenia wyglądu elementów w danej kolumnie. Wystarczy taki obiekt przypisać do właściwości GridViewColumn.CellTemplate.

W celu prezentacji działania omawianych mechanizmów stworzyłem klasę Driver (zawiera ona informacje o kierowcy Formuły 1 wraz ze ścieżką do pliku z jego zdjęciem) oraz klasę DriversViewModel zawierającą kolekcję obiektów typu Driver:

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

    public Driver(string firstName, string lastName, int birthYear,
        string team, string photo)
    {
        FirstName = firstName;
        LastName = lastName;
        BirthYear = birthYear;
        Team = team;
        Photo = photo;
    }
}

public class DriversViewModel
{
    private ObservableCollection<Driver> f1Drivers;

    public ObservableCollection<Driver> F1Drivers
    {
        get { return f1Drivers; }
    }

    public DriversViewModel()
    {
        f1Drivers = new ObservableCollection<Driver>
        {
            new Driver("Fernando", "Alonso", 1981, "Ferrari",
                @"D:\Photo\AlonsoFernando.jpg"),
            new Driver("Jenson", "Button", 1980, "McLaren",
                @"D:\Photo\ButtonJenson.jpg"),
            new Driver("Lewis", "Hamilton", 1985, "McLaren",
                @"D:\Photo\HamiltonLewis.jpg"),
            new Driver("Kimi", "Raikkonen", 1979, "Lotus",
                @"D:\Photo\RaikkonenKimi.jpg"),
            new Driver("Michael", "Schumacher", 1969, "Mercedes",
                @"D:\Photo\SchumacherMichael.jpg"),
            new Driver("Sebastian", "Vettel", 1987, "Red Bull",
                @"D:\Photo\VettelSebastian.jpg"),
            new Driver("Felipe", "Massa", 1981, "Ferrari",
                @"D:\Photo\MassaFelipe.jpg")
        };
    }
}

Teraz możemy utworzyć okno zawierające kontrolkę ListView z widokiem ustawionym na GridView. Kontrolka wyświetla elementy kolekcji f1Drivers:

DriversView1.xaml.cs:

/// <summary>
/// Interaction logic for DriversView1.xaml
/// </summary>
public partial class DriversView1 : Window
{
    public DriversView1()
    {
        InitializeComponent();
        this.DataContext = new DriversViewModel();
    }
}

DriversView1.xaml:

<Window x:Class="WPFDataTemplate.DriversView1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DriversView1" WindowStartupLocation="CenterScreen" Width="480" Height="550">
    <Window.Resources>
        <DataTemplate x:Key="PhotoCell">
            <Image Height="60" Stretch="Uniform" Source="{Binding Photo}" />
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ListView HorizontalAlignment="Stretch" Margin="0" Name="listView1"
                  VerticalAlignment="Stretch" ItemsSource="{Binding F1Drivers}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Width="80" Header=""
                                    CellTemplate="{StaticResource PhotoCell}" />
                    <GridViewColumn Width="85" Header="Imię"
                                    DisplayMemberBinding="{Binding FirstName}" />
                    <GridViewColumn Width="95" Header="Nazwisko"
                                    DisplayMemberBinding="{Binding LastName}" />
                    <GridViewColumn Width="80" Header="Rocznik"
                                    DisplayMemberBinding="{Binding BirthYear}" />
                    <GridViewColumn Width="80" Header="Zaspół"
                                    DisplayMemberBinding="{Binding Team}" />
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

W powyższym przykładzie obiekt DataTemplate użyty został do określenia sposobu prezentacji danych w pierwszej kolumnie kontrolki. Obiekt ten (PhotoCell) został zdefiniowany w zasobach okna i składa się z elementu Image. Przypisanie tego obiektu do właściwości CellTemplate pierwszej kolumny spowoduje wyświetlenie w niej zdjęcia danego kierowcy. Oto wygląd stworzonego okna:

Domyślnie kontrolka ListView wyświetla elementy jeden pod drugim. Dzięki użyciu ItemsPanelTemplate możemy to zmienić i zdefiniować własny układ. Poniższy kod tworzy okno z kontrolką ListView, w której elementy wyświetlane są od lewej do prawej w trzech kolumnach. Dodatkowo każdy z elementów otrzymał wygląd zdefiniowany w obiekcie DataTemplate:

DriversView2.xaml.cs:

/// <summary>
/// Interaction logic for DriversView2.xaml
/// </summary>
public partial class DriversView2 : Window
{
    public DriversView2()
    {
        InitializeComponent();
        this.DataContext = new DriversViewModel();
    }
}

DriversView2.xaml:

<Window x:Class="WPFDataTemplate.DriversView2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DriversView2" WindowStartupLocation="CenterScreen" Width="585" Height="605">
    <Grid>
        <ListView Padding="10" ItemsSource="{Binding F1Drivers}">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Columns="3" />
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Border CornerRadius="15" BorderThickness="5" BorderBrush="Navy"
                            Background="AliceBlue" Width="170" Height="170" Padding="0">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="80" />
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*" />
                                <RowDefinition Height="28" />
                            </Grid.RowDefinitions>
                            <Image Grid.Row="0" Grid.Column="0" Margin="7"
                                   Stretch="Uniform" Source="{Binding Photo}" />
                            <StackPanel Grid.Row="0" Grid.Column="1" Margin="0,20,0,20">
                                <Label Content="{Binding FirstName}" />
                                <Label Content="{Binding LastName}" />
                                <Label Content="{Binding BirthYear}" />
                            </StackPanel>
                            <Label Grid.Row="1" Grid.ColumnSpan="2" Width="120"
                                   Background="Navy" FontSize="14" Foreground="White"
                                   HorizontalContentAlignment="Center"
                                   Content="{Binding Team}" />
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

Analogiczny kod z umieszczeniem obiektów ItemsPanelTemplate i DataTemplate w zasobach okna:

<Window x:Class="WPFDataTemplate.DriversView2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DriversView2" WindowStartupLocation="CenterScreen" Width="585" Height="605">
    <Window.Resources>
        <ItemsPanelTemplate x:Key="myItemsPanelTemplate">
            <UniformGrid Columns="3" />
        </ItemsPanelTemplate>
        <DataTemplate x:Key="myDataTemplate">
            <Border CornerRadius="15" BorderThickness="5" BorderBrush="Navy"
                    Background="AliceBlue" Width="170" Height="170" Padding="0">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="80" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="28" />
                    </Grid.RowDefinitions>
                    <Image Grid.Row="0" Grid.Column="0" Margin="7"
                           Stretch="Uniform" Source="{Binding Photo}" />
                    <StackPanel Grid.Row="0" Grid.Column="1" Margin="0,20,0,20">
                        <Label Content="{Binding FirstName}" />
                        <Label Content="{Binding LastName}" />
                        <Label Content="{Binding BirthYear}" />
                    </StackPanel>
                    <Label Grid.Row="1" Grid.ColumnSpan="2" Width="120"
                           Background="Navy" FontSize="14" Foreground="White"
                           HorizontalContentAlignment="Center"
                           Content="{Binding Team}" />
                </Grid>
            </Border>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ListView Padding="10"
                  ItemsSource="{Binding F1Drivers}"
                  ItemsPanel="{StaticResource myItemsPanelTemplate}"
                  ItemTemplate="{StaticResource myDataTemplate}"/>
    </Grid>
</Window>

W powyższym przykładzie obiekt DataTemplate jest już bardziej rozbudowany niż poprzednio i dzięki niemu poszczególne elementy ListView otrzymały zupełnie inny wygląd. Z kolei użyty w ItemsPanelTemplate obiekt UniformGrid pozwolił na określenie dla nich nowego układu. Oczywiście w tym celu możemy posłużyć się także innymi kontenerami (np. WrapPanel). Tak wygląda utworzone okno:

Więcej informacji na MSDN: Data TemplatingItemsPanelTemplateDataTemplate