Pokazywanie postów oznaczonych etykietą Silverlight. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Silverlight. Pokaż wszystkie posty

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.

czwartek, 31 marca 2011

Konwersja danych w Silverlight i WPF - ValueConverter i StringFormatting

W dzisiejszym poście poruszę temat sposobu prezentacji danych użytkownikowi - a właściwie formatowania danych. Poruszam temat z dwóch powodów. Pierwszym jest to, iż wiedza ta jest potrzebna aby zrozumieć przyszły artykuł (dla tych którzy jeszcze nie posiadają wiedzy o konwersjach). Drugim powodem jest to, iż wielu deweloperów stara się formatować dane w kolekcji przed pokazaniem ich użytkownikowi. W tym celu najczęściej przechodzi się po kolekcji i odpowiedniego modyfikuje dane pobrane z np. z bazy danych czy też pliku. W WPFie są dwa bardzo wygodne mechanizmy dzięki którym unikniemy takiego zachowania.
Poruszany temat możemy wykorzystać zarówno w przypadku Silverlight jak i WPF. W obu przypadkach cała procedura przebiega w taki sam sposób. Przykład dla odmiany pokażę w WPF jednak tak jak wcześniej pisałem w Silverlight wszystko przebiega w taki sam sposób.

Rozpoczniemy od napisania przykładowej aplikacji:

Aplikacja pobiera dane z bazy danych AdventureWorksLT którą można pobrać ze stron microsoftu jako przykłady baz danych wraz z testowymi danymi danymi.
Kod który jest wymagany aby utworzyć pokazaną aplikację:

Klasa Product:



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

namespace WpfApplication1
{
    public class Product
    {
        public string Name { get; set; }
        public string Color { get; set; }
        public decimal StandardCost { get; set; }
        public DateTime SellStartDate { get; set; }
    }
}

Klasa DataAccess:



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

namespace WpfApplication1
{
    public class DatabaseAccess
    {
        public ObservableCollection<Product> GetProducts()
        {
            ObservableCollection<Product> products = new ObservableCollection<Product>();
            using (SqlConnection conn = new SqlConnection(@"Data Source=ACER5738;Initial Catalog=AdventureWorksLT;Integrated Security=True"))
            {
                using(SqlCommand cmd = new SqlCommand("SELECT name, color, standardCost, sellStartDate FROM salesLT.Product", conn))
                {
                    conn.Open();
                    using (SqlDataReader dr = cmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Product tmpProduct = new Product();
                            tmpProduct.Color = dr["Color"].ToString();
                            tmpProduct.Name = dr["Name"].ToString();
                            tmpProduct.SellStartDate = (DateTime)dr["SellStartDate"];
                            tmpProduct.StandardCost = (decimal)dr["StandardCost"];
                            products.Add(tmpProduct);
                        }
                    }
                }
            }

            return products;
        }
    }
}

Okno:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private DatabaseAccess dataAccess;

        public MainWindow()
        {
            InitializeComponent();
            dataAccess = new DatabaseAccess();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            dgridProducts.ItemsSource = dataAccess.GetProducts();
        }
    }
}

Warstwa prezentacji (XAML):



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">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2.4*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <DataGrid Name="dgridProducts">
            
        </DataGrid>
        
        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="0.8*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Product Name:" />
            <TextBlock Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.Name}" />
            <TextBlock Grid.Row="1" Text="Color:" />
            <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.Color}" />
            <TextBlock Grid.Row="2" Text="Standard Cost:" />
            <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.StandardCost}" />
            <TextBlock Grid.Row="3" Text="Sell Start Date:" />
            <TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.SellStartDate}" />
        </Grid>
    </Grid>
</Window>

Tak więc podstawowe "klocki" tworzące naszą aplikację są gotowe. Co w tej aplikacji nie podoba się na pierwszy rzut oka? Kilka rzeczy:
- design
- Standard Cost - 4 miejsca po przecinku zamiast standardowo dwóch
- Sell Stard Date - źle sformatowana data - mało czytelne
- nazwy kolumn

W tym poście zajmiemy się dwoma środkowymi problemami.
Aplikacja, którą napisaliśmy korzysta ze standardowego schematu:
1. Pobierz dane z bazy
2. Wyświetl dane użytkownikowi.

Taki schemat ma w domyśle niejawnie zapisane - dane są prawidłowe oraz są odpowiednio sformatowane. Oczywiście rzadko zdarza się pracować na idealnych danych. Dlatego aby poradzić sobie z sytuacjami, w których musimy dokonać obróbki wyświetlanych danych, WPF wprowadza nowe narzędzia w porównaniu do WindowsForms:
- string formatting - bardzo prosta a zarazem użyteczna właściwość pod nazwą Binding.StringFormat. Pozwala na zdecydowanie w jaki sposób będą wyświetlane ciągi znakowe.
- value converters - pozwala na dowolną konwersję między różnymi typami danych i zwracanie ich do bindowanej kontrolki

String Formatting
Pierwszy omawiany typ formatowania (string formatting) pozwala w łatwy sposób modyfikować łańcuchy tekstowe. Ustawienie właściwości Binding.StringFormat można zdefiniować do instrukcji:
{0:X} - gdzie 0 to pierwszy argument a X to format który chcemy zaaplikować dla danego tekstu.
W naszym pierwszym przypadku, czyli wyświetlania waluty możemy zastosować format:
{0:C} - gdzie C to standardowy sposób wyświetlania waluty w danym regionie. Wynik będzie następujący:

Kod formatujący dane wygląda następująco:


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

Dodając jedną właściwość, uzyskaliśmy pożądany wygląd. Warto zrwrócić na zastosowanie pustych nawiasów {}. Taki zabieg wymagany jest tylko w wypadku, gdy wartość właściwości StringFormat zamierzamy rozpocząć od nawiasu klamrowego. W innym przypadku np. StringFormat=Value is {0:C} - nawiasy nie są potrzebne.
Inne przydatne sposoby formatowania:
P - procenty
F[n] - formatuje liczbę rzeczywistą, zaokrąglając ją do n znaków po przecinku
d - krótka data
D - długa data


Value converter
Value converter to dużo bardziej zaawansowane narzędzie do konwersji typów. Pozwala na skonwertowanie jednego typu do dowolnego innego. Przykładów gdzie można użyć takiego zabiegu można sporo znaleźć w codziennej pracy:
- konwersja danych do postaci znakowej
- tworzenie specyfinczego typu dla WPF (np. z danych w postaci binarnej tworzyć obraz)
- zmiana właściwości w zależności od konwertowanej wartości - np. podświetlając na czerwono komórkę w wypadku błędnych danych.
Kroki które należy przejść aby skorzystać z dobrodziejstwa tej technologi są następujące:
1. Tworzymy klasę implementującą interfejs IValueConverter.
2. Dodajemy do stworzonej klasy atrybut ValueConversion, mówiący o typie z którego jest konwertowana wartość na docelowy typ.
3. Implementujemy metodę Convert, która zmienia typ wejściowy na żądany typ do wyświetlenia.
4. Implementujemy metodę ConvertBack, która definiuje jak ma zostać zmieniona wartość w drugą stronę - np. jak ma zostać zapisana do bazy danych

Zobaczmy więc przykład związany z konwersją daty.
Na początek klasa konwertujące, stworzona według podanych wyżej zasad:


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

namespace WpfApplication1
{
    [ValueConversion(typeof(DateTime), typeof(string))]
    class DataConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.GlobalizationCultureInfo culture)
        {
            DateTime dateTime = (DateTime)value;
            return string.Format("{0:MM/dd/yy}", dateTime);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.GlobalizationCultureInfo culture)
        {
            string date = value.ToString();
            DateTime result;
            if (DateTime.TryParse(date, out result))
            {
                return result;
            }
            return value;
        }
    }
}

Samo użycie sprowadza się do stworzenia obiektu konwertera i przypisaniu go do odpowiedniej właściwości podczas bindowania:


Code:
<Window.Resources>
        <local:DataConverter x:Key="DataConverter" />
    </Window.Resources>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding ElementName=dgridProducts, Path=SelectedItem.SellStartDate, Converter={StaticResource ResourceKey=DataConverter}}"  />

Efekt ostateczny:



W tym przypadku skorzystaliśmy z prostego zamienienia daty z jednego formatu na inny. Jednak w codziennej pracy można zetknąć się z innymi ciekawymi zagadnieniami:
- pobranie danych z bazy w formacie binarny i zamiana ich na obrazek
- zmiana koloru komórki w gridzie, elementu listy itp. na podstawie aktualnej wartości

czwartek, 24 marca 2011

Silverlight Toolkit - Charting

Jak wcześniej napisałem, zamierzam pociągnąć serię artykułów nt. Silverlight. Samą definicję co to jest Silverlight można odnaleźć w internecie (np w wikipedii - http://en.wikipedia.org/wiki/Microsoft_Silverlight). Silverlight działa w podobny sposób jak adobe flash. Instaluje się jako plugin do przeglądarki (ok. 4 MB do pobrania) i w założeniu jest multiplatformowy. Myślę, że na więcej informacji teoretycznych przyjdzie jeszcze czas, teraz przejdźmy do sedna :)

Podczas obecnie tworzonego projektu potrzebowałem przedstawić klientowi dane w formie wykresu. Jak wiadomo, jeden diagram potrafi powiedzieć więcej niż 4 strony tekstu.
Pomocny okazał się pakiet całkowicie darmowych kontrolek Silverlight toolkit, który można pobrać ze strony - http://silverlight.codeplex.com/
Zawiera on szereg dodatkowych kontrolek, które ułatwiają codzienną pracę dewelopera. Pakiet zawiera także kilka rodzajów wykresów z których skorzystałem podczas tworzenia aplikacji.

Dane w aplikacji którą tworzyłem (i w dalszym ciągu rozwijam) pobierane są z webserwisu (WCF). W przykładzie obrazującym działanie wykresów, wykorzystam prostsze rozwiązanie, polegające na zwrócenie danych przez metodę klasy. Jako rodzaj wykresów wybrałem wykres kołowy.

Tak więc przystępujemy do działania:
1. Na początek tworzymy nowy projekt Silverlight w Visual Studio 2010
2. Dodajemy na formatkę element Chart oraz PieSeries:
3. Stworzymy naszą klasę która zwróci dane do wyświetlenia na wykresie. Będzie to prosta klasa zwracająca średnie wynagrodzenie kwartalnie:


        public IEnumerable<SalaryItem<double>> GetQuarterlySalary(double startValue)
        {
            IList<SalaryItem<double>> quarterlySalary = new List<SalaryItem<double>>();
            for (int i = 1; i < 13; i += 4)
            {
                quarterlySalary.Add(new SalaryItem<double> { ItemName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(i), Value = startValue });
                startValue *= 1.02;
            }

            return quarterlySalary;
        }

Składnik wypłaty to prosta klasa z dwoma właściwościami:

    public class SalaryItem<T>
    {
        public string ItemName { get; set; }
        public T Value { get; set; }
    }

5. Źródło danych podłączymy po załadowaniu aplikacji silverlight:

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            PieSeries pie = chrtAverageSalary.Series[0] as PieSeries;
            if (pie != null)
            {
                pie.ItemsSource = new AverageSalary().GetQuarterlySalary(3400);  
            }
        }

6. W kodzie XAML dodajemy informacje, jakie pole odpowiada w źródle danych za nazwę w legendzie jak i która właściwość to wartość dla danego składnika:

        <toolkit:Chart Name="chrtAverageSalary" Title="Average Salary">
            <toolkit:PieSeries IndependentValueBinding="{Binding ItemName}" DependentValueBinding="{Binding Value}">
            </toolkit:PieSeries>
        </toolkit:Chart>

9. Całość wygląda następująco po uruchomieniu:

Prosty wykres stworzony. Dostajemy także możliwość podglądu wartości koła poprzez najechanie na odpowiednią część.

W dalszej części artykułu zostaną omówione bardziej skomplikowane aspekty, głównie związane z sposobem prezentacji danych.