WPF進階技巧和實戰08-依賴屬性與繫結03

蝸牛的希望發表於2021-11-01

資料提供者

在大多數的程式碼中,都是通過設定元素的DataContext屬性或者列表控制元件的ItemsSource屬性,從而提供頂級的資料來源。當資料物件是通過另一個類構造時,可以有其他選擇。

一種是作為視窗的資源定義資料物件。如果能夠使用宣告的方式構造物件,這種方法工作的很好,但是如果需要在執行時使用資料庫等方式獲取資料,這種技術就沒有意義了。但是會出現部分開發人員採用這種方法,基本思路是在建構函式中獲取所需的資料。

<Window.Resources>
    <local:PhotoList x:Key="MyPhotos" />
</Window.Resources>

public class Photo
{
    public Photo(string path, string eyeFlag)
        {
            _source = path;

            if (!File.Exists(path)) myBmp = null;
            try
            {
                myBmp = new BitmapImage();
                myBmp.BeginInit();
                myBmp.CacheOption = BitmapCacheOption.OnLoad;
                myBmp.StreamSource = new MemoryStream(File.ReadAllBytes(path));
                myBmp.EndInit();
                myBmp.Freeze();
            }
            catch (Exception)
            {
                myBmp = null;
            }

            EyeFlag = eyeFlag;
        }

    public override string ToString()
        {
            return Source;
        }

    private string _source;
    public string Source { get { return _source; } }

    private BitmapImage myBmp;
    public BitmapImage MyBmp
        {
            get { return myBmp; }
            set { myBmp = value; }
        }

    private string _EyeFlag;
    public string EyeFlag
        {
            get { return _EyeFlag; }
            set
            {
                _EyeFlag = value;
                //this.OnPropertyChanged("EyeFlag");
            }
        }
}

public class PhotoList : ObservableCollection<Photo>
{
    public void FilePathAdd(string bmpFile, string eyeFlag = "")
    {
        Add(new Photo(bmpFile, ""));
    }
}

此處的PhotoList類繼承自ObservableCollection類,因此他能夠儲存列表,可以通過方法或者直接在建構函式中填充資料。現在其他元素就可以在他們的繫結中使用這個資源了。

<ListBox ItemsSource="{Binding Source={StaticResource MyPhotos}}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

這種方法看起來可以,但是有一定風險。當新增錯誤處理時,需要將錯誤處理程式碼放在PhotoList類中。這種方式將資料模型、資料訪問程式碼以及使用者介面程式碼混合在一起,所以當需要訪問外部資源時,這種方法就很不合理了。
資料提供者是這種模型的擴充套件,可以通過資料提供者直接繫結帶在標記的資源部分定義的物件,然而,不是直接繫結到資料物件自身,而是繫結到能夠檢索或者構建資料物件的資料提供者。如果資料提供者能夠在發生異常時引發事件並提供用於配置與其操作相關細節的屬性,這種方法就合理。但是WPF的資料提供者還沒有達到這個標準,導致不值得使用這種方法。WPF提供了兩種資料提供者:

  • ObjectDataProvider,資料提供者通過呼叫另一個類中的方法獲取資訊
  • XmlDataProvider,直接從XML檔案獲取資訊

ObjectDataProvider

ObjectDataProvider能夠從應用程式的另一個類中獲取資訊。

  • 能夠建立需要的物件併為建構函式傳遞引數
  • 能夠呼叫所建立物件中的方法,並向他傳遞方法引數
  • 能夠非同步建立資料物件(能夠視窗載入之前一直等待,之後在後臺完成工作)
<Window.Resources>
    <ObjectDataProvider x:Key="MyPhotosProvider" MethodName="GetAll" ObjectType="{x:Type local:PhotoList}">
        <ObjectDataProvider.MethodParameters />
    </ObjectDataProvider>
    <local:PhotoList x:Key="MyPhotos" />
</Window.Resources>
<Grid>
    <ListBox ItemsSource="{Binding Source={StaticResource MyPhotos}}">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
    <ListBox ItemsSource="{Binding Source={StaticResource MyPhotosProvider}}">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Grid>

  1. 錯誤提示

當建立視窗時,XAML建立視窗並呼叫GetAll()方法,從而設定繫結。如果GetAll()方法返回期望的資料,一切沒有問題。如果丟擲異常,這事異常從視窗的建構函式中的InitializeComponent();向上傳輸,顯示此視窗的程式碼就要捕獲這個異常,即使在建構函式中捕獲這個異常,其餘程式碼也無法正常展示。

  1. 非同步支援

只要將IsAsynchronous="True"就可以在後臺程式中執行工作。

非同步資料繫結

WPF還通過繫結每個物件的IsAsyn屬性來提供非同步支援。WPF非同步地從資料物件檢索繫結屬性,資料物件自身仍然是同步建立的。一旦建立集合,繫結就會非同步地從Phone物件中查詢相關屬性,這個過程沒有什麼意義。利用這一屬性的唯一方法是構建在屬性獲取過程中新增耗時邏輯的特殊類,例如,考慮一個繫結到資料模型的分析應用程式。資料物件可能包含一部分資訊,使用耗時計算對其進行計算。可使用非同步繫結這種屬性,並使用同步繫結繫結其他屬性,應用程式中的一些資訊會立即顯示,其他資訊會在準備後顯示。WPF還提供了繫結優先順序,基於非同步繫結可以優先繫結一些屬性。

XmlDataProvider

XmlDataProvider資料提供者被設計成只讀的,不具有提交資料修改的能力,而且不能處理來自其他源的XML資料(資料庫記錄、Web服務訊息等)。如果預見到需要修改XML或者需要將XML資料轉換成程式碼中能夠使用的物件形式,最好使用XML擴充套件支援。如果資料是以XML形式儲存,然後由頁面支援展示,那麼XmlDataProvider是最合理的選擇。

資料轉換

WPF提供了2種工具,可以進行資料轉換:

  • 字串格式化:可以設定Binding的StringFormat屬性對文字形式的資料進行轉換(例如包含日期和數字的字串)
  • 值轉換器:功能強大,使用該功能可以將任意型別的資料來源轉換為任意型別的物件表示,然後傳遞到關聯的控制元件

字串轉換器

具體形式為{0:C}。其中0代表第一個數值,C表示希望的資料格式。完整的格式是 {}{0:C},程式碼如下:

<StackPanel Orientation="Horizontal">
    <TextBlock Text="{Binding Date, StringFormat={}{0:F}}"/>
    <TextBlock Text="{Binding Date, StringFormat=當前日期:{0:F}}"/>
</StackPanel>

值轉換器簡介

值轉換器負責在目標中顯示資料之前轉換源資料,並且對於雙向繫結在將資料應用回源之前轉換新的目標值。使用方式如下:

  • 將資料格式化為字串表示形式。例如將數字轉換成貨幣字串
  • 建立特定型別的WPF物件。讀取二進位制資料並將其轉換成BitmapImage物件,繫結到Image控制元件
  • 根據繫結資料有條件地改變元素中的屬性。
  1. 使用值轉換器設定字串的格式

建立值轉換器,主要有4個步驟:

  • 建立一個實現了IValueConverter介面的類
  • 為該類宣告新增ValueConversion特性,並制定目標資料型別(可選)
  • 實現Convert方法,將原來的格式轉換為顯示的格式
  • 實現ConvertBack方法,該方法實現反向變換,將值從顯示格式轉換為原格式
public class EyeTypeForegroundConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Brush output = ResourceHelper.GetResource<Brush>("PrimaryReverseTextBrush");
        //Brush output = (Brush)Application.Current.Resources["PrimaryReverseTextBrush"];

        if (value != null && value != DependencyProperty.UnsetValue && parameter != null && parameter != DependencyProperty.UnsetValue)
        {
            int param = System.Convert.ToInt32(parameter);
            int eyeType = System.Convert.ToInt32(value);

            if (param == eyeType) output = ResourceHelper.GetResource<Brush>("InfoBrush");
        }

        return output;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

  1. 使用值轉換器建立物件

最典型的應用就是從二進位制資料轉換BitmapImage物件,用於Image控制元件展示影像

public class Path2BitmapImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return null;

        if (value is string imagePath)
        {
            //return imagePath;
            return ImageShareHelper.File2BitmapImage(imagePath);
        }
        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public static BitmapImage File2BitmapImage(string path)
{
    if (!string.IsNullOrEmpty(path) && File.Exists(path) && (Path.GetExtension(path).Equals(".bmp") || Path.GetExtension(path).Equals(".jpeg") || Path.GetExtension(path).Equals(".jpg") || Path.GetExtension(path).Equals(".png")))
    {
        BitmapImage bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.CacheOption = BitmapCacheOption.OnLoad;
        bitmap.StreamSource = new MemoryStream(File.ReadAllBytes(path));
        bitmap.EndInit();
        bitmap.Freeze();
        return bitmap;
    }
    else
    {
        return null;
    }
}

  1. 應用條件格式化

有些轉換器不是為了顯示格式化的資料,而是根據不同條件展示不同資料

public class DtoShowStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null) return null;

            string output = null;

            {
                if (value is DateTime input)
                {
                    if (parameter == null)
                    {
                        output = input.ToString();
                    }
                    else//, ConverterParameter=date, 此處獲得是字串型別
                    {
                        var param = parameter.ToString();
                        output = input.ToShortDateString();
                    }
                }
            }
            {
                if (value is SexType input)
                {
                    switch (input)
                    {
                        case SexType.Female:
                            output = Properties.Langs.Lang.SexFemale;
                            break;
                        case SexType.Male:
                            output = Properties.Langs.Lang.SexMale;
                            break;
                        case SexType.Null:
                            output = Properties.Langs.Lang.SexNull;
                            break;
                        default:
                            break;
                    }
                }
            }
            {
                if (value is ExamFileType input)
                {
                    switch (input)
                    {
                        case ExamFileType.Jpg:
                            output = Properties.Langs.Lang.ExamFileTypeJpg;
                            break;
                        case ExamFileType.Avi:
                            output = Properties.Langs.Lang.ExamFileTypeAvi;
                            break;
                        case ExamFileType.Data:
                            output = Properties.Langs.Lang.ExamFileTypeData;
                            break;
                        default:
                            break;
                    }
                }
            }
            {
                if (value is EyeType input)
                {
                    switch (input)
                    {
                        case EyeType.Right:
                            output = Properties.Langs.Lang.EyeTypeRight;
                            break;
                        case EyeType.Left:
                            output = Properties.Langs.Lang.EyeTypeLeft;
                            break;
                        case EyeType.Both:
                            output = Properties.Langs.Lang.EyeTypeBoth;
                            break;
                        case EyeType.Unknown:
                            output = Properties.Langs.Lang.EyeTypeUnknown;
                            break;
                        default:
                            break;
                    }
                }
            }
            {
                if (value is ExamType input)
                {
                    switch (input)
                    {
                        case ExamType.SW8800:
                            output = Properties.Langs.Lang.ExamTypeSW8800;
                            break;
                        case ExamType.SW8000:
                            output = Properties.Langs.Lang.ExamTypeSW8000;
                            break;
                        case ExamType.SW6000D:
                            output = Properties.Langs.Lang.ExamTypeSW6000D;
                            break;
                        case ExamType.SW6000:
                            output = Properties.Langs.Lang.ExamTypeSW6000;
                            break;
                        case ExamType.SW9000:
                            output = Properties.Langs.Lang.ExamTypeSW9000;
                            break;
                        case ExamType.SW7000:
                            output = Properties.Langs.Lang.ExamTypeSW7000;
                            break;
                        case ExamType.SW6000A:
                            output = Properties.Langs.Lang.ExamTypeSW6000A;
                            break;
                        case ExamType.SW9800:
                            output = Properties.Langs.Lang.ExamTypeSW9800;
                            break;
                        case ExamType.SW4000:
                            output = Properties.Langs.Lang.ExamTypeSW4000;
                            break;
                        case ExamType.SW4000T:
                            output = Properties.Langs.Lang.ExamTypeSW4000A;
                            break;
                        case ExamType.SW900:
                            output = Properties.Langs.Lang.ExamTypeSW900;
                            break;
                        case ExamType.SW8800T:
                            output = Properties.Langs.Lang.ExamTypeSW8800T;
                            break;
                        case ExamType.SW9600:
                            output = Properties.Langs.Lang.ExamTypeSW9600;
                            break;
                        default:
                            break;
                    }
                }
            }
            {
                if (value is RoleType input)
                {
                    switch (input)
                    {
                        case RoleType.Admin:
                            output = Properties.Langs.Lang.UserRoleTypeAdmin;
                            break;
                        case RoleType.Company:
                            output = Properties.Langs.Lang.UserRoleTypeCompany;
                            break;
                        case RoleType.Doctor:
                            output = Properties.Langs.Lang.UserRoleTypeDoctor;
                            break;
                        default:
                            break;
                    }
                }
            }
            {
                if (value is LangType input)
                {
                    switch (input)
                    {
                        case LangType.zh:
                            output = Properties.Langs.Lang.LanguageZh;
                            break;
                        case LangType.en:
                            output = Properties.Langs.Lang.LanguageEn;
                            break;
                        case LangType.fa:
                            output = Properties.Langs.Lang.LanguageFa;
                            break;
                        case LangType.fr:
                            output = Properties.Langs.Lang.LanguageFr;
                            break;
                        default:
                            break;
                    }
                }
            }
            {
                if (value is IOLFormulaType input)
                {
                    switch (input)
                    {
                        case IOLFormulaType.SRKII:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaSRKII;
                            break;
                        case IOLFormulaType.SRKT:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaSRKT;
                            break;
                        case IOLFormulaType.Holladay:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaHolladay;
                            break;
                        case IOLFormulaType.BinkhorstII:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaBinkhorstII;
                            break;
                        case IOLFormulaType.HofferQ:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaHofferQ;
                            break;
                        case IOLFormulaType.Haigis:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaHaigis;
                            break;
                        case IOLFormulaType.HaigisL:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaHaigisL;
                            break;
                        case IOLFormulaType.ShammasPL:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaShammas;
                            break;
                        case IOLFormulaType.Masket:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaMasket;
                            break;
                        case IOLFormulaType.ModifiedMasket:
                            output = Properties.Langs.Lang.DeviceIolMasterIOLFormulaModifiedMasket;
                            break;
                        default:
                            break;
                    }
                }
            }
            {
                if (value is double input)
                {
                    if (parameter != null && parameter is string digits)
                    {
                        double roundDouble = (double)Math.Round((decimal)input, System.Convert.ToInt32(digits), MidpointRounding.AwayFromZero);
                        output = roundDouble.ToString(string.Format("f{0}", System.Convert.ToInt32(digits)));
                    }
                    else
                    {
                        output = input.ToString();
                    }
                }
            }
            return output;
        }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

  1. 評估多個屬性

到目前為止,已經使用繫結表示式將一部分源資料轉換成單個格式化的結果。也可以建立能夠評估或者結合多個源屬性資訊的繫結

/// <summary>
/// 多繫結 有一個為true 則顯示,否則隱藏,引數為0時,邏輯翻轉
/// </summary>
public class MultiOrEqualVisibleConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        Visibility output = Visibility.Collapsed;

        if (values != null && values.Count() > 0)
        {
            {
                bool param = true;
                if (parameter != null)
                {
                    param = System.Convert.ToBoolean(parameter);
                }
                var count = values.Where(p => p != DependencyProperty.UnsetValue && p != null && System.Convert.ToBoolean(p)).Count();
                if (count == 0)
                {
                    output = param ? Visibility.Collapsed : Visibility.Visible;
                }
                else
                {
                    output = param ? Visibility.Visible : Visibility.Collapsed;
                }
            }
        }
        return output;
    }

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

相關文章