niedziela, 1 maja 2011

Schema czyli jak pobrać informacje o tabeli w MS SQL

Często zdarza się że w programie potrzebujemy pobrać informację nt. tabeli. Np. chcemy dowiedzieć się jakiego typu jest dana kolumna czy też poznać długość kolumny znakowej. Przydaje się to np. kiedy chcemy stworzyć kolumnę w Gridzie która będzie mogła pomieścić cały string.

SQL Server przechowuje dane nt. schemy bazy danych w widokach które można znaleźć w schemie INFORMATION_SCHEMA.

Mamy tutaj takie widoki jak:
CHECK_CONSTRAINTS - zwraca informacje nt. checków w bazie
COLUMN_DOMAIN_USAGE - zwraca informacje do których obiektów bazy użytkownik ma dostęp
COLUMN_PRIVILEGES - zwraca informacje nt. tego czy dana kolumna ma przypisanego użytkownika
COLUMNS - zwraca wszystkie kolumny w danej bazie
CONSTRAINT_COLUMN_USAGE - zwraca ograniczenia nałożone na kolumny
CONSTRAINT_TABLE_USAGE - zwraca ograniczenia nałożone na tabelę
TABLES - zwraca wszystkie tabele w danej bazie
VIEW_COLUMN_USAGE - zwraca nazwy kolumn użytych w widokach
VIEWS - zwraca wszystkie widoki dla danej bazy
TABLE_CONSTRAINTS - zwraca ograniczenia nałożone na tabele

Pobranie np. wszystkich kolumn które tyczą się tabeli Address przedstawia się następująco:


Code:
SELECT *
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name='Address'

Oczywiście informacje w taki sposób są przydane, jednak aby wykorzystać je w projekcie przydałaby się jakaś klasa umożliwiająca łatwe ich pobranie. Stworzyłem przykładowy projekt umożliwiający pobranie podstawowych informacji nt. tabeli której zawartość chcemy zbadać. Zobaczmy na schemat:

Kilka słów objaśnienia na czym całość polega.
Na początku myślałem że stworzę prostą klasę która zwróci co trzeba i na tym się skończy cała zabawa. Jednak po przemyśleniu sprawy doszedłem do wniosku że takie podejście nie prowadzi do niczego dobrego. Chodzi przede wszystkim o możliwość dalszego rozwoju aplikacji i dodanie nowych możliwości.

DbSchema - główna klasa całego przedsięwzięcia. Zwraca Schemat danej tabeli za pomocą metody GetSchema. Tutaj następuje także inicjalizacja, jaki provider ma zostać użyty do wyciągnięcia danych z odnośnie schemy. Wykorzystuje tutaj wzorzec projektowy Strategy.

ISchemaProvider - interfejs z jedną metodą zwracającą schemę tabeli

ISchemaCreator - reprezentuje interfejs przedstawiający w jaki sposób będzie pozyskiwana schma tabeli

TableSchema - obiekt reprezentujący schemę tabeli - najważniejszą tutaj właściwością jest Columns czyli lista kolumn danej tabeli

ColumnInfo - przedstawia pojedynczą kolumnę w tabeli. Mamy takie informacje jak ograniczenie, typ danych, czy pole może przechowywać pola typu null, długość pola tekstowego oraz oczywiście nazwę kolumny

Constrains - typ wyliczeniowy reprezentujący ograniczenia mogące być nałożone na kolumnę

Dwie klasy pomocnicze SqlDataTypes oraz SqlConstrains pozwalają w łatwy sposób zwrócić wartość odpowiedniego typu wyliczeniowego.

Sam kod biblioteki przedstawia się następująco:

DbSchema:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;

namespace DbSchema
{
    public class DbSchema
    {
        #region Propeteries and fields
        private ConnectionStringSettings connectionStringSettings;
        /// <summary>
        /// Connection string settings (from app.config for exaple)
        /// </summary>
        public ConnectionStringSettings ConnectionStringSettings
        {
            get { return connectionStringSettings; }
            private set { connectionStringSettings = value; }
        }

        /// <summary>
        /// Schema provider 
        /// </summary>
        private ISchemaProvider schemaProvider;

        #endregion Propeteries and fields

        /// <summary>
        /// Default constructor
        /// </summary>
        /// <param name="connectionString">Connection string information (from App.setings for example)</param>
        public DbSchema(ConnectionStringSettings connectionStringSettings)
        {
            this.connectionStringSettings = connectionStringSettings;
            CreateProvider();
        }

        private void CreateProvider()
        {
            switch (connectionStringSettings.ProviderName)
            {
                case "System.Data.SqlClient":
                    schemaProvider = new SqlSchemaProvider(connectionStringSettings.ConnectionString);
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// Get Schema of table
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <returns></returns>
        public TableSchema GetSchema(string tableName)
        {
            return schemaProvider.GetTableSchema(tableName);
        }
    }
}

ColumnInfo:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;

namespace DbSchema
{
    public class ColumnInfo
    {
        private string name;
        /// <summary>
        /// Get Column name
        /// </summary>
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        private int length;
        /// <summary>
        /// Get column data length (especialy usesful for character fields)
        /// </summary>
        public int Length
        {
            get { return length; }
            set { length = value; }
        }

        private DbType dataType;
        /// <summary>
        /// Get column data type
        /// </summary>
        public DbType DataType
        {
            get { return dataType; }
            set { dataType = value; }
        }

        private Constraints constraint;
        /// <summary>
        /// Constrain for column
        /// </summary>
        public Constraints Constraint
        {
            get { return constraint; }
            set { constraint = value; }
        }

        private bool isnullAlble;
        /// <summary>
        /// Determin if column can be null
        /// </summary>
        public bool IsNullAble
        {
            get { return isnullAlble; }
            set { isnullAlble = value; }
        }

    }
}

Constraints:


Code:
using System;

namespace DbSchema
{
    public enum Constraints
    {
        /// <summary>
        /// No constrains on column
        /// </summary>
        None,
        /// <summary>
        /// Primary key
        /// </summary>
        PrimaryKey,
        /// <summary>
        /// Foreign key
        /// </summary>
        ForeignKey,
        /// <summary>
        /// Check
        /// </summary>
        Check
    }
}

ISchemaCeationProcess:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DbSchema
{
    public interface ISchemaCreationProcess
    {
        List<ColumnInfo> GetColumns(string tableName);
        void AddConstraints(string tableName, List<ColumnInfo> columns);
    }
}

ISchemaProvider:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DbSchema
{
    public interface ISchemaProvider
    {
        /// <summary>
        /// Getting Schema for table
        /// </summary>
        /// <param name="tableName">Table name for which we want to get schema</param>
        /// <returns>Return table schema</returns>
        TableSchema GetTableSchema(string tableName);
    }
}

SqlConstraints:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DbSchema
{
    public static class SqlConstrains
    {
        public static Constraints GetConstrains(string constrains)
        {
            Constraints constrain = Constraints.PrimaryKey;
            switch (constrains)
            {
                case "FOREIGN KEY":
                    constrain = Constraints.ForeignKey;
                    break;
                case "PRIMARY KEY":
                    constrain = Constraints.PrimaryKey;
                    break;
                case "CHECK":
                    constrain = Constraints.Check;
                    break;
            }

            return constrain;
        }
    }
}

SqlDataTypes:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;

namespace DbSchema
{
    public static class SqlDataTypes
    {
        public static DbType GetType(string type)
        {
            DbType dbType = DbType.String;
            switch (type)
            {
                case "int":
                    dbType = DbType.Int32;
                    break;
                case "nvarchar":
                    dbType = DbType.String;
                    break;
                case "datetime":
                    dbType = DbType.DateTime;
                    break;
            }

            return dbType;
        }
    }
}

SqlSchemaProvider:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Configuration;

namespace DbSchema
{
    public class SqlSchemaProvider : ISchemaProvider, ISchemaCreationProcess
    {
        private string connectionString;

        public SqlSchemaProvider(string connectionString)
        {
            this.connectionString = connectionString;
        }

        public TableSchema GetTableSchema(string tableName)
        {
            TableSchema tableSchema = new TableSchema(tableName);
            List<ColumnInfo> columns = GetColumns(tableName);
            tableSchema.Columns = columns;
            AddConstraints(tableName, columns);
            return tableSchema;
        }

        public List<ColumnInfo> GetColumns(string tableName)
        {
            List<ColumnInfo> columns = new List<ColumnInfo>();
            using (SqlConnection conn = new SqlConnection(connectionString))
            {
                using (SqlCommand cmd = new SqlCommand())
                {
                    cmd.Connection = conn;
                    cmd.CommandText = @"select column_name, is_nullable, data_type, character_maximum_length
                                        from INFORMATION_SCHEMA.COLUMNS
                                        WHERE table_name=@table";
                    cmd.Parameters.AddWithValue("@table", tableName);
                    try
                    {
                        conn.Open();
                        using (SqlDataReader dr = cmd.ExecuteReader())
                        {
                            while (dr.Read())
                            {
                                ColumnInfo columnInfo = new ColumnInfo();
                                columnInfo.Name = (string)dr["column_name"];
                                columnInfo.DataType = SqlDataTypes.GetType((string)dr["data_type"]);
                                columnInfo.IsNullAble = (string)dr["is_nullable"] == "YES" ? true : false;
                                if (dr["character_maximum_length"] != DBNull.Value)
                                {
                                    columnInfo.Length = int.Parse(dr["character_maximum_length"].ToString());
                                }
                                columns.Add(columnInfo);
                            }
                            dr.Close();
                        }
                    }
                    catch (Exception ex)
                    {
                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }

            return columns;
        }

        public void AddConstraints(string tableName, List<ColumnInfo> columns)
        {
            using (SqlConnection conn = new SqlConnection(connectionString))
            {
                using (SqlCommand cmd = new SqlCommand())
                {
                    cmd.Connection = conn;
                    cmd.CommandText = @"SELECT constraint_type, column_name
                                        FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu
                                         ON tc.CONSTRAINT_NAME = cu.CONSTRAINT_NAME 
                                        WHERE tc.table_name=@table";
                    cmd.Parameters.AddWithValue("@table", tableName);
                    try
                    {
                        conn.Open();
                        using (SqlDataReader dr = cmd.ExecuteReader())
                        {
                            while (dr.Read())
                            {
                                string columnName = (string)dr["column_name"];
                                columns.Find(x => x.Name == columnName).Constraint = SqlConstrains.GetConstrains(dr["constraint_type"].ToString());
                                dr.Close();
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
        }
    }
}

TableSchema:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DbSchema
{
    public class TableSchema
    {
        #region Fields and propeteries
        private List<ColumnInfo> columns;
        /// <summary>
        /// Column list for table
        /// </summary>
        public List<ColumnInfo> Columns
        {
            get { return columns; }
            set { columns = value; }
        }

        private string tableName;
        /// <summary>
        /// Table name for which schema is creating
        /// </summary>
        public string TableName
        {
            get { return tableName; }
            set { tableName = value; }
        }
        
        #endregion Fields and propeteries

        /// <summary>
        /// Default constructor 
        /// </summary>
        /// <param name="tableName">Table name</param>
        public TableSchema(string tableName)
        {
            this.tableName = tableName;
        }
    }
}


Powyższa implementacja oczywiście nie wyczerpuje tematu. Wraz z rozwojem dojdą z pewnością nowe typy, pola oraz oczywiście same providery.

Powyższa implementacja miała za zadanie pokazanie w jaki sposób można się dobrać do informacji nt. schemy tabel w MS SQL Serverze.

wtorek, 5 kwietnia 2011

Silverlight Toolkit - Charting cz. 2

Wracam do tematu który poruszałem całkiem nie tak dawno a dotyczył on tworzeniu wykresów w Silverlight (wszystko co tutaj jest opisane tyczy się także WPF - przykłady są pisane przy użyciu Silverlight).
Pisałem tam o bardziej skomplikowanych rzeczach niż tylko samo stworzenie wykresu. Kolejnym etapem będzie zmiana wyglądu tooltipa któremu oprócz standardowych informacji dodamy możliwość wyświetlania na jakim polu z legendy się znajduje.






Jak widać w podstawowej wersji oprócz samego wyglądu to i same informacje które przekazuje nie przekonują do polubienia go.
Spróbujmy na początek zmienić treść podpowiedzi. W tym celu potrzebujemy zmodyfikować styl, który tworzy podpowiedź. Skąd wziąć styl, który jest domyślnie ustawiony dla wykresu? Otóż domyślne style dla każdego z wykresów znajdują się w plikach typ_wykresuDataPoint. Folder ze stylami znajduje się w miejscu gdzie zainstalowany został silverlight toolkit. W moim przypadku ścieżka do folderu to:
C:\Program Files (x86)\Microsoft SDKs\Silverlight\v4.0\Toolkit\Apr10\Source\Source code.zip\Controls.DataVisualization.Toolkit\Charting\DataPoint
Tak więc z folderu tego pobieram interesujący mnie plik stylów, czyli PieDataPoint.
Następnie tworzymy nowy słownik zasobów w projekcie (Silverlight Resource Dictionary). Wklejamy do niego następnie definicję stylu znajdującego się w pliku PieDataPoint. Po tym etapie nasz słownik powinien zawierać następujące dane:


Code:
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="charting:PieDataPoint">
        <Setter Property="Background" Value="Orange"/>
        <Setter Property="BorderBrush" Value="White"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="IsTabStop" Value="False"/>
        <Setter Property="RatioStringFormat" Value="{}{0:p2}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="charting:PieDataPoint">
                    <Grid
                        x:Name="Root"
                        Opacity="0">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:0.1"/>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <DoubleAnimation
                                            Storyboard.TargetName="MouseOverHighlight"
                                            Storyboard.TargetProperty="Opacity"
                                            To="0.6"
                                            Duration="0"/>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="SelectionStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:0.1"/>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Unselected"/>
                                <VisualState x:Name="Selected">
                                    <Storyboard>
                                        <DoubleAnimation
                                            Storyboard.TargetName="SelectionHighlight"
                                            Storyboard.TargetProperty="Opacity"
                                            To="0.6"
                                            Duration="0"/>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="RevealStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:0.5"/>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Shown">
                                    <Storyboard>
                                        <DoubleAnimation
                                            Storyboard.TargetName="Root"
                                            Storyboard.TargetProperty="Opacity"
                                            To="1"
                                            Duration="0"/>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Hidden">
                                    <Storyboard>
                                        <DoubleAnimation
                                            Storyboard.TargetName="Root"
                                            Storyboard.TargetProperty="Opacity"
                                            To="0"
                                            Duration="0"/>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Path
                            x:Name="Slice"
                            Data="{TemplateBinding Geometry}"
                            Fill="{TemplateBinding Background}"
                            Stroke="{TemplateBinding BorderBrush}"
                            StrokeMiterLimit="1">
                            <ToolTipService.ToolTip>
                                <StackPanel>
                                    <ContentControl Content="{TemplateBinding FormattedDependentValue}"/>
                                    <ContentControl Content="{TemplateBinding FormattedRatio}"/>
                                </StackPanel>
                            </ToolTipService.ToolTip>
                        </Path>
                        <Path
                            x:Name="SelectionHighlight"
                            Data="{TemplateBinding GeometrySelection}"
                            Fill="Red"
                            StrokeMiterLimit="1"
                            IsHitTestVisible="False"
                            Opacity="0"/>
                        <Path
                            x:Name="MouseOverHighlight"
                            Data="{TemplateBinding GeometryHighlight}"
                            Fill="White"
                            StrokeMiterLimit="1"
                            IsHitTestVisible="False"
                            Opacity="0"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Najważniejszą dla nas częścią jest ta na samym praktycznie końcu:


Code:
<ToolTipService.ToolTip>
                                <StackPanel>
                                    <ContentControl Content="{TemplateBinding FormattedDependentValue}"/>
                                    <ContentControl Content="{TemplateBinding FormattedRatio}"/>
                                </StackPanel>
                            </ToolTipService.ToolTip>

Przed przystąpieniem do operacji uruchommy aplikację i zobaczmy czy coś się nie zmieniło:





Niestety. Jak widać cały wykres korzysta tylko z jednego koloru - pomarańczowego. Należy w takim wypadku ręcznie ustawić kolory dla danych przychodzących.
Nad tym problemem powiem - posiedziałem trochę. Na szczęście google zna odpowiedź na praktycznie każde pytanie i udało się odnaleźć rozwiązanie. 
Zanim zagooglowałem, próbowałem różnych rozwiązań. Na początek jako ofiarę wybrałem właściwość Palette. Pierwsza więc próba wyglądała następująco:


Code:
<vis:ResourceDictionaryCollection x:Key="MyPaletteColors">
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Red"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Green"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Blue"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Orange"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Purple"/>
                </Style>
            </ResourceDictionary>
        </vis:ResourceDictionaryCollection>

Niestety po ustawieniu właściwości w wykresie nic się nie zmieniło:


Code:
<toolkit:Chart Name="chrtAverageSalaary" Title="Average Salary">
            <toolkit:PieSeries DataPointStyle="{StaticResource PieStyle}" IndependentValueBinding="{Binding ItemName}" DependentValueBinding="{Binding Value}" 
                               Palette="{StaticResource MyPalette}"/>
        </toolkit:Chart>

Wykres nadal pozostawał pomarańczowy. No nic następnie spróbowałem zbindować paletę do właściwości globalnej wykresu:


Code:
<toolkit:Chart Name="chrtAverageSalaary" Title="Average Salary"  Palette="{StaticResource MyPalette}">
            <toolkit:PieSeries DataPointStyle="{StaticResource PieStyle}" IndependentValueBinding="{Binding ItemName}" DependentValueBinding="{Binding Value}" 
                              />
        </toolkit:Chart>

Jednak i tu szczał okazał się nie trafny. Stwierdzając, że niestety na dalsze próby nie ma zbyt wiele czasu rozpocząłem poszukiwania rozwiązania mojego problemu. Po kilku minutach poszukiwać znalazłem rozwiązanie problemu, które okazało się prostsze niż myślałem.
Paleta kolorów nadal była potrzebna, jednak zamiast ustawiać styl dla DataPointStyle, należy ustawić jego wartość w palecie kolorów. Spójrzmy na przykład który obrazuje poszukiwane rozwiązanie:

Okno:


Code:
<UserControl x:Class="Charting.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400" 
    xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit" Loaded="UserControl_Loaded"
    xmlns:vis="clr-namespace:System.Windows.Controls.DataVisualization;assembly=System.Windows.Controls.DataVisualization.Toolkit">
    <Grid x:Name="LayoutRoot" Background="White" >
        <toolkit:Chart Name="chrtAverageSalaary" Title="Average Salary">
            <toolkit:PieSeries Palette="{StaticResource MyPalette}" IndependentValueBinding="{Binding ItemName}" DependentValueBinding="{Binding Value}"   />
        </toolkit:Chart>
    </Grid>
</UserControl>

Styl:


Code:
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:charting="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    xmlns:vis="clr-namespace:System.Windows.Controls.DataVisualization;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    >

    <ControlTemplate TargetType="charting:PieDataPoint" x:Key="MyPieDataPointTemplate">
        <Grid
            x:Name="Root"
            Opacity="0">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualStateGroup.Transitions>
                        <VisualTransition GeneratedDuration="0:0:0.1"/>
                    </VisualStateGroup.Transitions>
                    <VisualState x:Name="Normal"/>
                    <VisualState x:Name="MouseOver">
                        <Storyboard>
                            <DoubleAnimation
                                Storyboard.TargetName="MouseOverHighlight"
                                Storyboard.TargetProperty="Opacity"
                                To="0.6"
                                Duration="0"/>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
                <VisualStateGroup x:Name="SelectionStates">
                    <VisualStateGroup.Transitions>
                        <VisualTransition GeneratedDuration="0:0:0.1"/>
                    </VisualStateGroup.Transitions>
                    <VisualState x:Name="Unselected"/>
                    <VisualState x:Name="Selected">
                        <Storyboard>
                            <DoubleAnimation
                                Storyboard.TargetName="SelectionHighlight"
                                Storyboard.TargetProperty="Opacity"
                                To="0.6"
                                Duration="0"/>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
                <VisualStateGroup x:Name="RevealStates">
                    <VisualStateGroup.Transitions>
                        <VisualTransition GeneratedDuration="0:0:0.5"/>
                    </VisualStateGroup.Transitions>
                    <VisualState x:Name="Shown">
                        <Storyboard>
                            <DoubleAnimation
                                Storyboard.TargetName="Root"
                                Storyboard.TargetProperty="Opacity"
                                To="1"
                                Duration="0"/>
                        </Storyboard>
                    </VisualState>
                    <VisualState x:Name="Hidden">
                        <Storyboard>
                            <DoubleAnimation
                                Storyboard.TargetName="Root"
                                Storyboard.TargetProperty="Opacity"
                                To="0"
                                Duration="0"/>
                        </Storyboard>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
            <Path
                x:Name="Slice"
                Data="{TemplateBinding Geometry}"
                Fill="{TemplateBinding Background}"
                Stroke="{TemplateBinding BorderBrush}"
                StrokeMiterLimit="1">
                <ToolTipService.ToolTip>
                    <StackPanel>
                        <ContentControl Content="{TemplateBinding FormattedDependentValue}"/>
                        <ContentControl Content="{TemplateBinding FormattedRatio}"/>
                    </StackPanel>
                </ToolTipService.ToolTip>
            </Path>
            <Path
                x:Name="SelectionHighlight"
                Data="{TemplateBinding GeometrySelection}"
                Fill="Red"
                StrokeMiterLimit="1"
                IsHitTestVisible="False"
                Opacity="0"/>
            <Path
                x:Name="MouseOverHighlight"
                Data="{TemplateBinding GeometryHighlight}"
                Fill="White"
                StrokeMiterLimit="1"
                IsHitTestVisible="False"
                Opacity="0"/>
        </Grid>
    </ControlTemplate>
    
    <vis:ResourceDictionaryCollection x:Key="MyPalette">
        <!-- Blue -->
        <ResourceDictionary>
            <RadialGradientBrush x:Key="Background" GradientOrigin="-0.1,-0.1" Center="0.075,0.015" RadiusX="1.05" RadiusY="0.9">
                <GradientStop Color="#FFB9D6F7"/>
                <GradientStop Color="#FF284B70" Offset="1"/>
            </RadialGradientBrush>
            <Style x:Key="DataPointStyle" TargetType="Control">
                <Setter Property="Template" Value="{StaticResource MyPieDataPointTemplate}"/>
                <Setter Property="Background" Value="{StaticResource Background}"/>
            </Style>
        </ResourceDictionary>
        <!-- Red -->
        <ResourceDictionary>
            <RadialGradientBrush x:Key="Background" GradientOrigin="-0.1,-0.1" Center="0.075,0.015" RadiusX="1.05" RadiusY="0.9">
                <GradientStop Color="#FFFBB7B5"/>
                <GradientStop Color="#FF702828" Offset="1"/>
            </RadialGradientBrush>
            <Style x:Key="DataPointStyle" TargetType="Control">
                <Setter Property="Template" Value="{StaticResource MyPieDataPointTemplate}"/>
                <Setter Property="Background" Value="{StaticResource Background}"/>
            </Style>
        </ResourceDictionary>
        <!-- Light Green -->
        <ResourceDictionary>
            <RadialGradientBrush x:Key="Background" GradientOrigin="-0.1,-0.1" Center="0.075,0.015" RadiusX="1.05" RadiusY="0.9">
                <GradientStop Color="#FFB8C0AC"/>
                <GradientStop Color="#FF5F7143" Offset="1"/>
            </RadialGradientBrush>
            <Style x:Key="DataPointStyle" TargetType="Control">
                <Setter Property="Template" Value="{StaticResource MyPieDataPointTemplate}"/>
                <Setter Property="Background" Value="{StaticResource Background}"/>
            </Style>
        </ResourceDictionary>
    </vis:ResourceDictionaryCollection>
</ResourceDictionary>

Tak więc pierwszy etap - przywrócenie kolorów mamy za sobą. Czas zabrać się za ToolTip. Aby dodać informację na temat tego na czym aktualnie stoimy wystarczy dopisać jedną linijkę:


Code:
<ToolTipService.ToolTip>
                    <ToolTip>
                        <StackPanel>
                            <ContentControl Content="{TemplateBinding FormattedDependentValue}"/>
                            <ContentControl Content="{TemplateBinding FormattedRatio}"/>
                            <ContentControl Content="{TemplateBinding FormattedIndependentValue}"/>
                        </StackPanel>
                    </ToolTip>
                </ToolTipService.ToolTip>

Uzyskany efekt będzie następujący:

niedziela, 3 kwietnia 2011

MultiValueConverter oraz coś o bindowaniu i wyświetlaniu danych w kontrolkach wykorzystujących listy

W poprzednim artykule opisałem w jaki sposób konwertować pojedyncza wartość. W tym artykule pokażę, w jaki sposób radzić sobie z łączeniem wielu wartości. Oprócz tego omówię zastosowanie szablonów i stylów podczas bindowania do kolekcji typu list. Tak jak poprzednio informacje tyczą się zarówno Silverlight jak i WPF. Przykłady będą oparte o WPF - zastosowanie w Silverlight jest takie samo.


1. Konwersja kilku wartości w jedną
Korzystając z ostatniego przykładu (poprzedni post), spróbujemy wyświetlić cenę i datę jej wprowadzenia, efekt o który nam chodzi wygląda następująco:


Aby uzyskać taki efekt możemy postąpić na dwa sposoby. Do tego celu wykorzystamy ostatnio poznane właściwości ale w wersji multi:

String Format:


Code:
<TextBlock Grid.Row="4" Grid.ColumnSpan="2">
                <TextBlock.Text>
                    <MultiBinding StringFormat="{}{0:C}, {1:d}">
                        <Binding ElementName="dgridProducts" Path="SelectedItem.StandardCost" />
                        <Binding ElementName="dgridProducts" Path="SelectedItem.SellStartDate" />
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>

Wykorzystaliśmy tutaj String Format. Teraz kolejny przykład wykorzystujący ValueInStockConverter. Jest to mechanizm podobny do poprzednio pokazanego Value converter. Najważniejszą różnicą jest to, iż otrzymuje on jako argument tablicę obiektów wartości z których tworzymy wynik:


Code:
<TextBlock Grid.Row="4" Grid.ColumnSpan="2">
                <TextBlock.Text>
                    <MultiBinding Converter="{StaticResource dpc}">
                        <Binding ElementName="dgridProducts" Path="SelectedItem.StandardCost" />
                        <Binding ElementName="dgridProducts" Path="SelectedItem.SellStartDate" />
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;

namespace WpfApplication1
{
    public class DatePriceConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, System.GlobalizationCultureInfo culture)
        {
            decimal price = (decimal)values[0];
            DateTime sellDate = (DateTime)values[1];

            return string.Format("{0:C}, {1:d}", price, sellDate);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.GlobalizationCultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Wynik w drugim przypadku jest taki sam jak w pierwszym.


2. Kontrolki wykorzystujące listy do wyświetlania elementów
W tym rozdziale postaram się przybliżyć kilka ciekawych technik pozwalających w łatwy sposób osiągnąć ciekawy efekt wizualny, podczas bindowania do kontrolek typu listy jak np.drzewa, listy, pasek narzędzi, elementy menu, pasek stanu itd. Zbudowane są one z małych bloczków zwanych elementami. Element to np. przycisk na pasku narzędzi. W przypadku tych kontrolek mamy możliwość bindowania do dwóch kluczowych własności: stylu i szablonu.

Podstawowe właściwości tych kontrolek to m.in.:
ItemSource - źródło danych
DisplayMemberPath - właściwość która ma zostać wyświetlona dla poszczególnych elementów
ItemStringFormat - działa w taki sam sposób jak wcześciej omawiany Binding.StringFormat
ItemContainerStyle - pozwala na ustawienie stylu konteneru wyświetlanego elementu
ItemContainerStyleSelector - pozwala na ustawienie różnych styli dla różnych elementów listy
AlternationCount - pozwala na wyświetlanie na przemian stylów - np wartość dwa spowoduje wyświetlanie danego stylu co drugi element
ItemTemplate - pozwala nadać szablon dla tworzonych elementów
ItemsPanel - Panel na którym umieszczone są wszystkie elementy (najczęściej VirtualizingStackPanel

O ile 3 pierwsze właściwości są zapewne wszystkim znane i wykorzystywane, o tyle warto wspomnieć o pozostałych.

ItemContainerStyle
Przejdźmy od razu do przykładu:


Code:
<ListBox Name="lboxProducts" DisplayMemberPath="Name" Width="250">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Background" Value="BlueViolet" />
                    <Setter Property="Margin" Value="4" />
                    <Setter Property="Padding" Value="4" />
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="Background" Value="Yellow" />
                            <Setter Property="Foreground" Value="White" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>

W łatwy sposób zmieniliśmy wygląd elementów ListBoxa. Możemy teraz dla przykładu zmienić poszczególne elementy na konkretne kontrolki np. RadioButtony czy też CheckBoxy:


Code:
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="554" Width="996" Loaded="Window_Loaded"
        xmlns:local="clr-namespace:WpfApplication1">
    <Window.Resources>
        <Style x:Key="RadioButtonListStyle" TargetType="{x:Type ListBox}">
            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style TargetType="{x:Type ListBoxItem}" >
                        <Setter Property="Margin" Value="2" />
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                    <RadioButton Focusable="False" IsChecked="{Binding Path=IsSelected, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent} }">
                                        <ContentPresenter></ContentPresenter>
                                    </RadioButton>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <ListBox Name="lboxProducts" DisplayMemberPath="Name" Width="250" Style="{StaticResource ResourceKey=RadioButtonListStyle}">
        </ListBox>
    </Grid>
</Window>

Zmiana stylu co w n-tym elemencie
Bardzo często można spotkać się z efektem zmiany stylu co n-tego elementu, np. co drugi posiada dodatkowy cień. Silverlight jak i WPF posiada wbudowany mechanizm umożliwiający uzyskanie takiego elementu w bardzo łatwy i szybki sposób. Zobaczmy na przykład:


Code:
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="554" Width="996" Loaded="Window_Loaded"
        xmlns:local="clr-namespace:WpfApplication1">
    <Grid>
        <ListBox Name="lboxProducts" DisplayMemberPath="Name" Width="250" AlternationCount="2">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Style.Triggers>
                        <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                            <Setter Property="Background" Value="LightBlue" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </Grid>
</Window>

Efekt wywołania:



Style selector
Jeżeli potrzebujemy, aby styl został przypisany pod względem odpowiedniego warunku np. w zależności od danych zawartych w konkretnym elemencie, musimy posłużyć się implementacją własnej klasy, która dziedziczy po klasie StyleSelector. 
Zobaczmy na przykład:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;

namespace WpfApplication1
{
    class ProductByColorSelector : StyleSelector
    {
        public override System.Windows.Style SelectStyle(object item, System.WindowsDependencyObject container)
        {
            Product product = (Product)item;
            string style = string.Empty;
            Window window = Application.Current.MainWindow;
            Style returnStyle = null;
            switch (product.Color)
            {
                case "Red":
                    returnStyle = (Style)window.FindResource("Red");
                    break;
                case "Blue":
                    returnStyle = returnStyle = (Style)window.FindResource("Blue");;
                    break;
                case "Yellow":
                    returnStyle = returnStyle = (Style)window.FindResource("Yellow");
                    break;
                default:
                    return base.SelectStyle(item, container);
                    break;
            }

            return returnStyle; 
        }
    }
}

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="554" Width="996" Loaded="Window_Loaded"
        xmlns:local="clr-namespace:WpfApplication1">
    <Window.Resources>
        <Style x:Key="Yellow" TargetType="{x:Type ListBoxItem}">
            <Setter Property="Background" Value="LightYellow" />
            <Setter Property="Padding" Value="2" />
        </Style>
        <Style x:Key="Red" TargetType="{x:Type ListBoxItem}">
            <Setter Property="Background" Value="OrangeRed" />
            <Setter Property="Padding" Value="2" />
        </Style>
        <Style x:Key="Blue" TargetType="{x:Type ListBoxItem}">
            <Setter Property="Background" Value="AliceBlue" />
            <Setter Property="Padding" Value="2" />
        </Style>
        <local:ProductByColorSelector x:Key="styleSelector" />
    </Window.Resources>
    <Grid>
        <ListBox Name="lboxProducts" DisplayMemberPath="Name" Width="250" ItemContainerStyleSelector="{StaticResource ResourceKey=styleSelector}">
        </ListBox>
    </Grid>
</Window>

Efekt który uzyskamy będzie następujący:


Oczywiście można się posłużyć bardziej skomplikowaną klasą bardziej generyczną i np. jako nazwy stylów wprowadzić właściwości i odpowiednio wprowadzić je podczas deklaracji stylu dla kontrolki.


Tyle na dzisiaj odnośnie stylów dla elementów w listboxach i podobnych kontrolkach. Informacje które tutaj zawarte są (jak i w poprzednim poście) przydadzą się aby zrozumieć kolejny mój post odnośnie zmiany wyglądu ToolTipa dla wykresów.