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

蝸牛的希望發表於2021-10-09

將元素繫結在一起

資料繫結最簡單的形式是:源物件是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>


相關文章