2014年1月24日 星期五

IValueConverter 與 DataBinding 的結合應用

對於在定義 GUI 應用程式中的資料結構時,時常會為了 UI 上的某些特殊效果,刻意在類別中建立一些用來顯示的 UIElement 例如 SolidColorBrush 等,這些為了顯示而加入的 Property 不僅降低程式碼的可讀性,也會造成資料結構因為要描述視覺效果而不夠乾淨,在 Presentation Foundation 相關框架中有一些方法可以用來解決這種問題,IValueConverter 就是一個很方便又直覺的方法。

假設我有一個 ListBox 且裡面放了 6 個項目,我想要在特殊情況時讓某個項目被 Highlight 起來,這個項目要和其他一般的項目有著不同的視覺效果,舉個例子來比較一下特殊項目及一般項目的外觀:

一般項目:
黑色字體
字體 12 px
背景白色

特殊項目:
淺藍色字體
字體 15 px
背景灰色

在一般的情況下我可能會定義這樣子的資料結構來使用

public class DataItem
{
    // 為了顯示而建立的屬性
    public Color BackgroundColor { get; set; }
    public Int32 FontSize { get; set; }
    public SolidColorBrush FontBrush { get; set; }

    // 以下才是真正屬於 DataItem 的屬性
    public Boolean Special { get; set; }
    public String Header { get; set; }
    public String Text { get; set; }
}

這種情況下我的列表愈大,DataItem 中額外的 Property 所造成的影響也就愈大,所以我將最上面三個 Property 移除,改成利用 Special 這個 Property 配合 Converter 來達到一樣的效果,還給 DataItem 這個資料結構該有的樣子。

首先定義三個 Converter,分別根據 Special 這個布林值給出對應的外觀型態

// 將布林值轉成顏色
public class BooleanToHighlightBackgroundConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value is Boolean && (Boolean)value) ? Colors.Gray : Colors.White;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Colors color = value as Colors;
        return Colors.Gray.Equals(color);
    }
}

// 將布林值轉成字體尺寸
public class BooleanToHighlightFontSizeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value is Boolean && (Boolean)value) ? 15 : 12;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value is Int32 && (Int32) value == 15;
    }
}

// 將布林值轉成字體色刷
public class BooleanToHighlightFontColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value is Boolean && (Boolean)value) ? new SolidColorBrush(Colors.AliceBlue) : new SolidColorBrush(Colors.Black);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        SolidColorBrush brush = value as SolidColorBrush;
        return brush.Color == Colors.AliceBlue;
    }
}

同時將 DataItem 改成更乾淨的 Model

public class DataItem : INotifyPropertyChanged
{
    private Boolean special;
    public Boolean Special
    {
        get
        {
            return special;
        }
        set
        {
            special = value;
            NotifyPropertyChange("Special");
        }
    }

    private String header;
    public String Header
    {
        get
        {
            return header;
        }
        set
        {
            header = value;
            NotifyPropertyChange("Header");
        }
    }

    private String text;
    public String Text
    {
        get
        {
            return text;
        }
        set
        {
            text = value;
            NotifyPropertyChange("Text");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChange(String propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

在 MainWindow.xaml 中加入剛才建立的三個 Converter 為靜態資源,並將列表中照剛才的描述套用上合適的 Converter 如下

<Window x:Class="ConverterPractice.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ConverterPractice"
        Title="MainWindow" Height="350" Width="150">
 
    <Window.Resources>
        <local:BooleanToHighlightBackgroundConverter x:Key="BackgroundConverter" />
        <local:BooleanToHighlightFontSizeConverter x:Key="FontSizeConverter" />
        <local:BooleanToHighlightFontColorConverter x:Key="FontColorConverter" />
    </Window.Resources>
 
    <Grid>
        <ListBox ItemsSource="{Binding Items}" HorizontalContentAlignment="Stretch">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <Grid.Background>
                            <SolidColorBrush Color="{Binding Special, Converter={StaticResource BackgroundConverter}}" />
                        </Grid.Background>
                        <TextBlock Grid.Row="0" Text="{Binding Header}"
                                   Foreground="{Binding Special, Converter={StaticResource FontColorConverter}}"
                                   FontSize="{Binding Special, Converter={StaticResource FontSizeConverter}}" />
                        <TextBlock Grid.Row="1" Text="{Binding Text}"
                                   Foreground="{Binding Special, Converter={StaticResource FontColorConverter}}"
                                   FontSize="{Binding Special, Converter={StaticResource FontSizeConverter}}" />
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

最後我撰寫一個 Timer 來每秒改變 Highlight 項目作為示範

public partial class MainWindow : Window
{
    public MainWindow()
        {
            InitializeComponent();

            items = new List<DataItem>();
            items.Add(new DataItem { Special = false, Header = "Header A", Text = "Text A" });
            items.Add(new DataItem { Special = false, Header = "Header B", Text = "Text B" });
            items.Add(new DataItem { Special = false, Header = "Header C", Text = "Text C" });
            items.Add(new DataItem { Special = false, Header = "Header D", Text = "Text D" });
            items.Add(new DataItem { Special = false, Header = "Header E", Text = "Text E" });
            items.Add(new DataItem { Special = false, Header = "Header F", Text = "Text F" });

            DataContext = this;

            timer = new DispatcherTimer();
            timer.Interval = TimeSpan.FromSeconds(1.0);
            timer.Tick += OnTimerTick;
            timer.Start();
        }

    private void OnTimerTick(object sender, EventArgs e)
        {
            Int32 highlightIndex = (DateTime.UtcNow.Second % 6);
            for (Int32 i = 0; i < 6; ++i)
            {
                items[i].Special = i == highlightIndex;
            }
        }

    private DispatcherTimer timer;

    private List<DataItem> items;
    public List<DataItem> Items
        {
            get
            {
                return items;
            }
        }
}

使用 Converter 後存放資料的類別更乾淨了,需要改變項目外觀時,也不需在多個地方調整,僅需調整對應的 Converter 即可,效果如下



沒有留言:

張貼留言