【UWP】實現 FindAncestor 綁定


在 WPF 里,我們是可以在 RelativeSource 上面實現的,舉個例子:

<Grid Tag="2">
    <Button>
        <Grid Tag="1">
            <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Grid, AncestorLevel=2}, Path=Tag, Mode=OneWay}" />
        </Grid>
    </Button>
</Grid>

將 RelativeSource 的 Mode 設置為 FindAncestor 就可以了。AncestorType 代表綁定的類型,AncestorLevel 代表查詢第幾個,默認是 1。所以在上面的例子里,由於 AncestorLevel 是 2,所以查詢出來的是 Tag 等於 2 的那個 Grid。假如設置成 3,那就查詢不到了。

 

但是,在 UWP 里,微軟出於性能考慮,把 FindAncestor 給去掉了,RelativeSource 的 Mode 只剩下了 Self 和 TemplateParent。但是需求永遠是存在的,那么總得有個解決方案吧,假如你搜索過 Google 或者 StackOverflow,無一不例外就是改成通過 ElementName 來綁定,也就是上面的例子會變成如下這樣:

<Grid x:Name="TargetGrid"
      Tag="2">
    <Button>
        <Grid Tag="1">
            <TextBlock Text="{Binding ElementName=TargetGrid, Path=Tag, Mode=OneWay}" />
        </Grid>
    </Button>
</Grid>

說實話這樣也能用,而且性能更好了,一直以來,我自己的 UWP 項目也是通過這種方式來解決。

 

但是,在前段時間我開發我自己的圖片緩存控件(https://github.com/h82258652/HN.Controls.ImageEx)時,就發現了一個無法解決的問題。圖片控件 ImageEx 提供了一個 DownloadProgress 的屬性可以獲取當前圖片的下載進度。另外該控件還有一個 LoadingTemplate 的屬性,可以設置一個模板,在圖片加載的過程顯示。現在我想在加載的時候顯示一個進度條,於是乎就有了以下代碼:

<controls:ImageEx x:Name="TargetImage">
    <controls:ImageEx.LoadingTemplate>
        <DataTemplate>
            <ProgressBar Maximum="1"
                         Value="{Binding ElementName=TargetImage, Path=DownloadProgress.Percentage, Mode=OneWay}" />
        </DataTemplate>
    </controls:ImageEx.LoadingTemplate>
</controls:ImageEx>

這樣單個 ImageEx 就沒問題了,但是需求再進一步,我需要所有的 ImageEx 都是這樣,LoadingTemplate 是一致的。在此刻,我們已經沒辦法通過綁定 ElementName 的方式來解決問題了。

 

俗話說,不行就包一層。XAML 里包一層的話,那就是 ContentControl 了,這里我們創建一個叫 AncestorBindingAssist 的模板控件,繼承自 ContentControl。

cs 代碼如下:

public class AncestorBindingAssist : ContentControl
    {
        public static readonly DependencyProperty AncestorLevelProperty = DependencyProperty.Register(nameof(AncestorLevel), typeof(int), typeof(AncestorBindingAssist), new PropertyMetadata(1, OnAncestorLevelChanged));
        public static readonly DependencyProperty AncestorTypeProperty = DependencyProperty.Register(nameof(AncestorType), typeof(Type), typeof(AncestorBindingAssist), new PropertyMetadata(default(Type), OnAncestorTypeChanged));
        public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(DependencyObject), typeof(AncestorBindingAssist), new PropertyMetadata(default(DependencyObject)));

        public AncestorBindingAssist()
        {
            DefaultStyleKey = typeof(AncestorBindingAssist);
        }

        public int AncestorLevel
        {
            get => (int)GetValue(AncestorLevelProperty);
            set => SetValue(AncestorLevelProperty, value);
        }

        public Type AncestorType
        {
            get => (Type)GetValue(AncestorTypeProperty);
            set => SetValue(AncestorTypeProperty, value);
        }

        public DependencyObject Source
        {
            get => (DependencyObject)GetValue(SourceProperty);
            private set => SetValue(SourceProperty, value);
        }

        protected override void OnApplyTemplate()
        {
            UpdateSource();
        }

        private static void OnAncestorLevelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var obj = (AncestorBindingAssist)d;
            var value = (int)e.NewValue;

            if (value < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(AncestorLevel));
            }

            obj.UpdateSource();
        }

        private static void OnAncestorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var obj = (AncestorBindingAssist)d;

            obj.UpdateSource();
        }

        private void UpdateSource()
        {
            Source = AncestorType == null ? null : this.GetAncestors().Where(temp => AncestorType.IsInstanceOfType(temp)).Skip(AncestorLevel - 1).FirstOrDefault();
        }
    }

AncestorType 和 AncestorLevel 兩個屬性跟 WPF 里一致,然后提供一個 Source 屬性提供給下級綁定。在 AncestorType 或者 AncestorLevel 發生變化時,則調用 UpdateSource 方法刷新 Source。UpdateSource 方法里的 GetAncestors 來自 WinRTXamlToolkit。

xaml 代碼的話就很簡單了,因為這里只是包一層。

<Style TargetType="local:AncestorBindingAssist">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:AncestorBindingAssist">
                    <ContentPresenter />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

接下來該怎么用呢,以文章開頭的例子,就變成了這樣:

<Grid Tag="2">
        <Button>
            <Grid Tag="1">
                <local:AncestorBindingAssist x:Name="BindingAssist"
                                             AncestorLevel="2"
                                             AncestorType="Grid">
                    <TextBlock Text="{Binding ElementName=BindingAssist, Path=Source.Tag, Mode=OneWay}" />
                </local:AncestorBindingAssist>
            </Grid>
        </Button>
    </Grid>

各位看官可能會吐槽,這跟直接綁定 ElementName 好像沒啥區別,而且還更復雜了。但是這里我們再舉上面我那個 ImageEx 的例子,現在我們想所有 ImageEx 復用 LoadingTemplate 就可以這么寫了:

<Style TargetType="controls:ImageEx">
    <Setter Property="LoadingTemplate">
        <Setter.Value>
            <DataTemplate>
                <local:AncestorBindingAssist x:Name="BindingAssist"
                                             AncestorType="controls:ImageEx">
                    <ProgressBar Maximum="1"
                                 Value="{Binding ElementName=BindingAssist, Path=Source.DownloadProgress.Percentage, Mode=OneWay}" />
                </local:AncestorBindingAssist>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

這樣就能所有的 ImageEx 都能復用 LoadingTemplate 了。而這是簡單的綁定 ElementName 所做不到的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM