WPF進階技巧和實戰08-依賴屬性與綁定02

將元素綁定在一起

數據綁定最簡單的形式是:源對象是WPF元素而且源屬性是依賴項屬性。依賴項屬性內置了更改通知支援,當源對象中改變依賴項屬性時,會立即更新目標對象的綁定屬性。

元素綁定到元素也是經常使用的,可以使元素的交互自動化,而不必編寫程式碼。

綁定表達式

Visibility="{Binding Path=IsChecked, 
ElementName=rdKer, 
Converter={StaticResource EqualVisibleConverter}, 
ConverterParameter=1}"

此處綁定設置兩個屬性,ElementName屬性指示源元素,Path屬性指示源元素的屬性。此處使用Path而不是Property,是因為Path可能指向屬性的屬性,也可能指向屬性的索引器,用於構建多層次路徑。

綁定錯誤

WPF不會引發異常類通知與數據綁定相關的問題,如果指定的元素不存在或者屬性不存在,那麼不會收到任何指示,只是不能在目標屬性中顯示數據。

綁定模式

名稱 說明
OneWay 當源屬性變化時更新目標屬性
TwoWay 源屬性變化時更新目標屬性,目標屬性變化時更新源屬性
OneTime 最初根據源屬性值設定目標屬性值。然而,其後的所有改變都會被忽略(除非綁定被設置為一個完全不同的對象或者調用UpdateTarget()方法)。如果源屬性不變化,可使用這種模式降低開銷
OneWayToSource 和OneWay類似,但方向相反。當目標屬性發生變化時更新源屬性,但目標屬性永遠不會被更新
Default 此類綁定依賴於目標屬性。既可以是雙向的,也可以是單向的。除非明確另一種模式,否則所有綁定都是用該方法。

使用程式碼創建綁定

基於標記的綁定比通過程式碼創建的綁定常見。除非一些特殊情況:

  • 創建動態綁定:如果希望根據運行時修改綁定,或者根據環境創建不同的綁定,這時使用程式碼創建綁定通常更合理(也可以在窗口的Resources集合中定義可能希望使用的每個綁定,並且添加使用合適的綁定對象調用SetBinding()的程式碼)
  • 刪除綁定:如果希望刪除綁定,從而可以通過普通方式設置屬性,需要藉助ClearBinding()或者ClearAllBindings()方法。僅為屬性應用新值是不夠的-如果正在使用雙向綁定,設置的值會傳播到鏈接的對象,並且兩個屬性保持同步。
  • 創建自定義控制項:為讓他人能夠更容易地修改您構建的自定義控制項外觀,需要將特定細節(如事件處理程式和數據綁定表達式)從標記移到程式碼中。

使用程式碼檢索綁定

可使用程式碼檢索綁定並檢查其屬性,而不必考慮綁定最初是用程式碼還是標記創建的。

  • 使用靜態方法BindingOperations.GetBinding()來檢索相應的Binding對象,分別傳入綁定元素和具有綁定表達式的屬性。可以通過Binding對象獲取其各種屬性
  • 通過調用BindingOperations.GetBindingExpression()方法獲取更加實用的BindingExpression對象,和上面的參數相同。

綁定更新

數據源的變化會立即影響目標,但是目標到源未必立即執行、這個行為由Binding.UpdateSourceTrigger屬性控制。

名稱 說明
PropertyChanged 當目標屬性發生變化時立即更新源
LostFocus 當目標屬性發生變化並且目標丟失焦點時更新源
Explicit 除非調用BindingExpression.UpdateSource()方法,否則無法更新
Default 根據目標屬性的元素確定更新行為(從技術角度看,是根據FrameworkPropertyMetadata.DefaultUpdateSourceTrigger屬性決定更新行為)。大多數默認是PropertyChanged,但是TextBox.Text是LostFocus

綁定延遲

在極少數情況下,需要防止數據綁定觸發操作和修改源對象,至少在某一段時間內時這樣的。可能要添加短暫的延時時間,避免過分頻繁地觸發操作,需要使用Binding對象的Delay屬性,等待數毫秒之後再提交更改。

綁定到非元素對象

在數據驅動的應用程式中,更多的操作是創建從不可見對象中提取數據的綁定表達式。唯一的要求是顯示的資訊必須是公有屬性,不能採用私有資訊或者欄位。當綁定到非元素對象時,需要設置數據源,而不是使用Binding.ElementName屬性

Source屬性

該屬性是指向源對象的引用,也就是提供數據的對象。

RelativeSource屬性

這是引用,使用RelativeSource對象指向源對象。可以在當前元素的基礎上構建引用,在編寫控制項模板以及數據模板是很方便。RelativeSource的4中模式:

名稱 說明
Self 表達式綁定到同一元素的另一個屬性上
FindAncestor 綁定到父元素,WPF將查找元素樹直到發現期望的父元素。為了指定父元素,還必須設置AncestorType屬性以提示希望查找的父元素的類型。還可以使用AncestorLevel屬性略過一定數量的特定元素。比如AncestorTpye={x:Type ListBoxItem}並且AncestorLevel=3,標識希望綁定到第3個ListBoxItem(從當前節點沿著樹查找),默認情況下AncestorLevel=1
PreviousData 表達式綁定到數據綁定列表中的前一個數據項,在列表項中會使用這種模式
TemplateParent 表達式綁定到應用模板的元素。只有當綁定位於控制項模板或者數據模板內部時,這種模式才會工作

DataContext屬性

如果沒有指定Source或者RelativeSource屬性數據源,WPF就從當前元素開始在元素樹上向上查找,直到第一個非空 的DataContext屬性。

數據綁定

WPF數據綁定允許創建從任何對象的任何屬性獲取資訊的綁定,並且可以使用創建的綁定填充任何元素的任何屬性。

使用自定義對象綁定到資料庫

資料庫中的提取資訊,然後轉換成自定義的對象中,將自定義對象作為數據源綁定到控制項中進行顯示某些屬性。

具有null值的綁定,在數據類中,可通過簡單的值類型使用可控數據類型來反應資料庫中可空的欄位。如果是引用類型,可以直接使用,具有null值。綁定null值的結果是可以預測的:對於數字欄位,這一行為能夠區分缺少數據和零值的情況。可以通過綁定表達式中設置TargetNullValue屬性類改變WPF對null值的處理方式。

綁定到對象集合

綁定到單個對象是非常直觀的。但是當需要綁定的對象是某些集合時,就需要使用更高級的元素。所有派生自ItemsControl的類都能夠顯示條目的完整列表。為了支援集合綁定,ItemsControl類定義了3個重要的屬性:

名稱 說明
ItemsSource 指向的集合包含將在列表中顯示的所有對象
DisplayMemberPath 確定用於為每個項創建顯示文本的屬性
ItemTemplate 接受的數據模板用於每個項創建可視化外觀。這個屬性比DispalyMemberPath屬性強大的多

只要是支援IEnumerable介面(數組、各種類型的集合以及許多特殊的封裝了數據項組的對象都支援該介面),都可以用來填充ItemsSource屬性。然而,基本的IEnumerable介面僅支援只讀綁定。

提高大列表性能

  1. 虛擬化

WPF列表控制項提供的最重要的功能是UI虛擬化(僅為當前顯示項創建容器對象的一種技術)。例如一個具有5000條記錄的ListBox控制項,但是可見區域只能包含10條記錄,ListBox只創建10個ListBoxItem對象,而不是全部記錄都創建。

UI虛擬化支援實際上沒有被構建進ListBox或者ItemsControl類,而是被硬編碼到VirtualizingStackPanel容器,除了支援虛擬化外,該面板和StackPanel面板的功能類似。ListBox、ListView以及DataGrid是自動使用虛擬化面板來布局其子元素,Commbox使用標準的沒有虛擬化的StackPanel面板,如果要支援虛擬化,就要明確地通過提供新的ItemsPanelTemplate來添加虛擬化支援。

<ComboBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120">
    <ComboBox.ItemTemplate>
        <ItemContainerTemplate>
            <VirtualizingStackPanel/>
        </ItemContainerTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

TreeView是另一個支援虛擬化的控制項,但在默認情況下,她關閉了改功能。可以通過屬性來啟用特性。

<TreeView VirtualizingPanel.IsVirtualizing="True"/>

一般情況下,對列表控制項進行綁定數據時,不會將很多的數據綁定到列表控制項,會使用分頁等操作進行少數數據的展示。

  1. 項容器再循環

通常當滾動支援虛擬化時,控制項不斷地創建新的項容器對象以保持新的可見項,如果啟用了項容器再循環,ListBox控制項將只保持少量ListBoxItem對象存活,當滾動時,通過新數據載入這些ListboxItem對象,從而重複使用他們。提高了滾動性能,降低了記憶體消耗。除DataGrid之外的所有控制項,該特性默認是禁用的。

VirtualizingPanel.VirtualizationMode=”Recycling”

  1. 快取長度

VirtualizingStackPanel創建了幾個超過其顯示範圍的附加項。這樣,在開始滾動時,就可以立即顯示這些項。可以使用CacheLength和CacheLengthUnit來選擇如何指定附加項的數量。

VirtualizingPanel.CacheLength=”10″ VirtualizingPanel.CacheLengthUnit=”Item”

CacheLengthUnit允許選擇如何指定附加項的數量:項數、頁數(其中,單頁包含適應於控制項可視窗口的所有項)或者像素數(如果項顯示不同大小的圖片,這個選擇更合理)

  1. 延遲滾動

為進一步提高滾動性能,可開啟延遲滾動特性。當用戶在滾動條上拖動滾動滑塊時不會更新列表顯示,只有用戶釋放滾定滑塊時才刷新。

ScrollViewer.IsDeferredScrollingEnabled=”True”

VirtualizingStackPanel通常使用基於項的滾動,這意味著當向下滾動少許時,下一項就顯示出來。無法滾動查看項的一部分,在面板上至少會滾動一個完整項。可以通過屬性覆蓋該行為:

VirtualizingPanel.ScrollUnit=”Pixel”

驗證

在任何數據綁定中,另一個要素是驗證(捕獲非法數值並拒絕這些非法數值的邏輯)。可直接在控制項中構建驗證(例如,通過響應文本框中的輸入並拒絕非法字元),這種低級的方法限制了靈活性。驗證提供了兩種方法用於捕獲非法值:

  • 可在數據對象中引發錯誤。為告知WPF發生了錯誤,只需要從屬性設置過程中拋出異常。通常,WPF會忽略所有在設置屬性時拋出的異常,但可以進行配置,從而顯示更有幫助的可視化指示。另一種選擇是在自定義的數據類中實現INotifyDataErrorInfo或IDataErrorInfo介面,從而可得到指示錯誤的功能而不會拋出異常
  • 可在綁定級別上定義驗證。這種方法可獲得使用相同驗證的靈活性,而不必考慮使用的是哪個控制項輸入。更好的是,因為是在不同的類中定義驗證,所以可很容易地在存儲類似數據類型的多個綁定中重用驗證。

只有當來自目標的值正被用於更新源時才會應用驗證,也就是只有當使用TwoWay模式或者OneWayToSource模式時的綁定才應用驗證。

  1. 在數據對象中進行驗證
public class MainWindowViewModel : NotifyPropertyBase
{
    private decimal? _productPrice;
    public decimal? ProductPrice
    {
        get => _productPrice;
        set
        {
            if (value.HasValue && value.Value < 0)
            {
                throw new Exception("ProductPrice 不能小於0");
            }
            else
            {
                _productPrice = value;
                OnPropertyChanged("ProductPrice");
            }
        }
    }
}

上面的示例中主要驗證價格不能小於0,但是不能為用戶提供任何與問題相關的回饋(WPF會忽略當設置和獲取屬性時發生的數據綁定錯誤),對於這種情況,用戶無法知道更新已經被拒絕。實際上,非法的值依然保留在文本框中,只是沒有被應用到綁定的數據對象。需要藉助於ExceptionValidationRule驗證規則。

  • ExceptionValidationRule驗證規則

ExceptionValidationRule是預先構建的驗證規則,它向WPF報告所有異常。要使用ExceptionValidationRule驗證規則,必須將他綁定到Binding.ValidationRules集合中,如下程式碼:

<TextBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120">
    <TextBox.Text>
        <Binding Path="ProductPrice">
            <Binding.ValidationRules>
                <ExceptionValidationRule/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

這個例子中,同時使用了值轉換器和驗證規則。通常是在轉換值之前進行驗證,但是ExceptionValidationRule比較特殊。它捕獲在任何位置發生的異常,包括當編輯的值不能轉換成正確數據類型時發生的異常、由屬性設置器拋出的異常以及由值轉換器拋出的異常。
當驗證失敗時,System.Windows.Controls.Validation類的附加屬性記錄下驗證錯誤。對於每個失敗的驗證規則,WPF分一下3個步驟:

  • 在綁定的元素上(TextBox控制項),將Validation.HasError附加屬性設置為true
  • 創建包含錯誤資訊的ValidationError對象(作為ValidationRule.Validate()方法的返回值)並將該對象添加到關聯的Validation.Errors集合中
  • 如果Binding.NotifyOnValidationError屬性設置為true,WPF就在元素上引發Validation.Error附加事件

當發生錯誤時,綁定控制項的可視化外觀也會發生變化。當控制項的Validation.HasError附加屬性設置為true時,WPF自動將控制項使用的模板切換成Validation.ErrprTemplate附加屬性定義的模板。在文本框中,新模板將文本框的輪廓改成一條紅色的邊框。

在大多數情況下,我們都希望用某種方式來增強提示,並提供與引發問題的錯誤相關的資訊。這樣Error事件才有意義。我們可以通過程式碼處理Error事件或者提供自定義模板,從而提供不同的可視化指示資訊。

  • INotifyDataErrorInfo介面

WPF通過介面IDataErrorInfo和INotifyDataErrorInfo,允許構建報告錯誤的對象而不是直接拋出異常。兩個介面具有相同的目標,即用更加人性化的錯誤通知系統來替代未處理的異常。IDataErrorInfo是初始的錯誤跟蹤介面,為了是WPF向下兼容。INotifyDataErrorInfo介面具有同樣的功能,但介面更加豐富。

public class MainWindowViewModel : NotifyPropertyBase, INotifyDataErrorInfo
{
    private decimal? _productPrice;
    public decimal? ProductPrice
        {
            get => _productPrice;
            set
            {
                if (value.HasValue && value.Value < 0)
                {
                    //throw new Exception("ProductPrice 不能小於0");
                    SetError("ProductPrice", new List<string> { "ProductPrice 不能小於0" });
                }
                else
                {
                    ClearErrors("ProductPrice");
                }

                _productPrice = value;
                OnPropertyChanged("ProductPrice");
            }
        }

    private Dictionary<string, List<string>> DictError = new Dictionary<string, List<string>>();

    private void SetError(string propertyName, List<string> propertyErrors)
        {
            DictError.Remove(propertyName);

            DictError.Add(propertyName, propertyErrors);

            if (ErrorsChanged != null)
            {
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

    private void ClearErrors(string propertyName)
        {
            DictError.Remove(propertyName);

            if (ErrorsChanged != null)
            {
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

    /// <summary>
    /// 用於指示數據對象是否包含錯誤資訊
    /// </summary>
    public bool HasErrors => DictError.Keys.Count > 0;

    /// <summary>
    /// 在添加和刪除錯誤時發生
    /// </summary>
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    /// <summary>
    /// 提供完整的錯誤資訊內容
    /// </summary>
    /// <param name="propertyName"></param>
    /// <returns></returns>
    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            return DictError.Values;
        }

        if (DictError.ContainsKey(propertyName))
        {
            return DictError[propertyName];
        }
        return null;
    }
}

<TextBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Margin="200,200,0,0">
    <TextBox.Text>
        <Binding Path="ProductPrice" ValidatesOnNotifyDataErrors="True" NotifyOnValidationError="True"/>
    </TextBox.Text>
</TextBox>

通過上述兩種形式實現異常通知,但是他們之間有本質的區別:觸發異常時,不會在數據對象中更新屬性。但是使用INotifyDataErrorInfo介面時,允許使用非法值,但是會標記出來,數據對象會被更新,可以使用通知或者事件告知用戶。

  1. 自定義驗證規則

應用自定義規則的方法和應用自定義轉化器的方法類似。繼承自ValidationRule,並重寫Validate()方法。

public class CustomValidation : ValidationRule
{
    private decimal? _minValue = 0;
    private decimal? _maxValue = int.MaxValue;

    public decimal? MinValue
    {
        get => _minValue;
        set => _minValue = value;
    }



    public decimal? MaxValue
    {
        get => _maxValue;
        set => _maxValue = value;
    }





    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        decimal price = 0;

        try
        {
            string strValue = value.ToString();
            if (!string.IsNullOrEmpty(strValue))
            {
                price = decimal.Parse(strValue, cultureInfo);
            }
        }
        catch (Exception)
        {

            return new ValidationResult(false, "非法數據");
        }

        if (price < _minValue || price > _maxValue)
        {
            return new ValidationResult(false, "非法數據");
        }

        return new ValidationResult(true, null);
    }
}

<TextBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Margin="200,300,0,0">
    <TextBox.Text>
        <Binding Path="ProductPrice">
            <Binding.ValidationRules>
                <local:CustomValidation MaxValue="99.99"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

通常會使用同類規則的每個元素定義不同的驗證規則對象。如果確實希望為多個綁定使用相同的驗證規則,可將驗證規則定義為資源,並在每個綁定中簡單的使用靜態標記指向該資源。
Binding.ValidationRules集合可包含任意數量的驗證規則。將值提交到源時,WPF將按照順序檢查每個驗證規則,如果所有規則驗證都成功了,WPF接著會調用轉化器為源應用值。

  1. 響應驗證錯誤

在上述例子中,有關用戶接收到錯誤的唯一指示是在違反規則的文本框周圍的紅色輪廓。為了提供更多資訊,可以處理Error事件,當存儲或者清楚錯誤時會引發該事件,但是前提是首先確保已經將 NotifyOnValidationError=”True”。

Error事件使用冒泡策略的路由事件,所以可通過在父容器中關聯事件處理程式來為多個控制項來處理Error事件。

<TextBox Width="120" Margin="200,300,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Validation.Error="TextBox_Error">
    <TextBox.Text>
        <Binding NotifyOnValidationError="True" Path="ProductPrice">
            <Binding.ValidationRules>
                <local:CustomValidation MaxValue="99.99" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

private void TextBox_Error(object sender, ValidationErrorEventArgs e)
{
    if(e.Action == ValidationErrorEventAction.Added)
    {
        MessageBox.Show(e.Error.ErrorContent.ToString());
    }
}

ValidationErrorEventArgs.Error屬性提供了一個ValidationError的對象,包含引起問題的異常(Exception),違反的驗證規則(ValidationRule),關聯的綁定對象(BindingInError)以及ValidationRule對象返回的任何自定義消息(ErrorContent)。

  1. 獲取錯誤列表

在某些情況下,可能需要獲取當前窗口所有的錯誤資訊,需要做的就是遍曆元素樹,獲取每個元素的屬性。

  1. 顯示不同的錯誤指示符號

可以通過定義自己的錯誤模板,以適當的方式來標識錯誤資訊。錯位模板是裝飾層,始終位於窗口內容之上的繪圖層。

<TextBox Width="120" Margin="200,300,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Validation.Error="TextBox_Error">
    <TextBox.Text>
        <Binding NotifyOnValidationError="True" Path="ProductPrice">
            <Binding.ValidationRules>
                <local:CustomValidation MaxValue="99.99" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <TextBlock Text="***" Foreground="Red"/>
        </ControlTemplate>
    </Validation.ErrorTemplate>
</TextBox>