Mô phỏng cơ chế RelativeSource Binding trong Silverlight

Trong các giải pháp ràng buộc dữ liệu truyền thống, thuộc tính DataContext thường đóng vai trò chính để kết nối giao diện với mô hình business. Tuy nhiên, trong nhiều kịch bản thiết kế UI, chúng ta cần liên kết trực tiếp các thành phần trình bày với nhau mà không phụ thuộc vào dữ liệu ứng dụng. Cơ chế RelativeSourceElementName trong WPF giải quyết tốt vấn đề này, nhưng Silverlight phiên bản cũ lại thiếu hỗ trợ nguyên bản. Kỹ thuật này sẽ mở rộng phương pháp sử dụng thuộc tính đính kèm (attached property) để tái tạo hoàn chỉnh khả năng tương tự của RelativeSource trong môi trường Silverlight.

1. Mở rộng lớp cấu hình ràng buộc

Để hỗ trợ các chế độ nguồn tương đối, lớp định nghĩa tham số ràng buộc cần được bổ sung thêm các trường điều khiển hành vi tìm kiếm và ánh xạ:

public class BindingConfig
{
    public string TargetPropertyName { get; set; }
    public string SourcePropertyName { get; set; }
    public string LinkedElementName { get; set; }
    public IValueConverter TransformConverter { get; set; }
    public object ConverterArg { get; set; }
    
    public bool IsSelf { get; set; }
    public string AncestorTypeName { get; set; }
    public int AncestorDepth { get; set; } = 1;
}

2. Triển khai bộ định tuyến sự kiện Loaded

Khi thuộc tính đính kèm được gán vào phần tử đích, hệ thống sẽ đăng ký lắng nghe sự kiện Loaded. Tại thời điểm phần tử được render xong, bộ xử lý sẽ phân tích cấu hình để xác định phương thức kết nối phù hợp:

private static void OnAttach(DependencyObject owner, DependencyPropertyChangedEventArgs args)
{
    var target = owner as FrameworkElement;
    if (target != null)
        target.Loaded += OnTargetRendered;
}

private static void OnTargetRendered(object sender, RoutedEventArgs e)
{
    var target = sender as FrameworkElement;
    var config = GetBindingConfig(target);

    if (!string.IsNullOrEmpty(config.LinkedElementName))
    {
        ProcessElementNameBinding(target, config);
    }
    else if (config.IsSelf)
    {
        SetupRelayLink(target, target, config);
    }
    else if (!string.IsNullOrEmpty(config.AncestorTypeName))
    {
        var source = LocateAncestor(target, config.AncestorTypeName, config.AncestorDepth);
        if (source != null)
        {
            SetupRelayLink(target, source, config);
        }
    }
}

Trường hợp IsSelf được kích hoạt, hệ thống sẽ gán cả nguồn và đích là cùng một đối tượng. Điều này rất hữu ích khi cần đồng bộ một thuộc tính kích thước dựa trên nội dung, chẳng hạn như chiều rộng của hộp nhập liệu tự động điều chỉnh theo độ dài chuỗi ký tự.

3. Cơ chế leo ngược cây trực quan (FindAncestor)

Đối với chế độ tìm kiếm tổ tiên, phương thức LocateAncestor sẽ thực hiện việc duyệt ngược qua VisualTreeHelper để tìm đúng loại đối tượng tại độ sâu chỉ định:

private static DependencyObject LocateAncestor(DependencyObject start, string typeName, int depth)
{
    var current = start;
    int foundCount = 0;

    while (current != null)
    {
        current = VisualTreeHelper.GetParent(current);
        if (current?.GetType().Name == typeName)
        {
            foundCount++;
            if (foundCount == depth)
                return current;
        }
    }
    return null;
}

Cấu trúc mã này cho phép linh hoạt xác định bậc tổ tiên cần nhắm đến mà không cần hardcode tên đối tượng. Dưới đây là ví dụ kết hợp nhiều kịch bản ràng buộc khác nhau trong cùng một vùng chứa:

<UserControl x:Class="DemoUI.MainPanel">
    <UserControl.Resources>
        <local:BoolToVisibility x:Key="VisConv"/>
    </UserControl.Resources>

    <StackPanel x:Name="RootContainer" Background="LightGray">
        <StackPanel Width="450" Margin="10">
            <!-- Đồng bộ chiều rộng theo nội dung văn bản -->
            <TextBox Text="150" Margin="0,4,0,0">
                <local:BindingHelper.Rule>
                    <local:BindingConfig TargetPropertyName="Width"
                                         SourcePropertyName="Text"
                                         IsSelf="True"/>
                </local:BindingHelper.Rule>
            </TextBox>

            <!-- Lấy chiều rộng từ UserControl cha -->
            <TextBox Margin="0,4,0,0">
                <local:BindingHelper.Rule>
                    <local:BindingConfig TargetPropertyName="Text"
                                         SourcePropertyName="Width"
                                         AncestorTypeName="MainPanel"/>
                </local:BindingHelper.Rule>
            </TextBox>

            <!-- Tham chiếu đến StackPanel ngoài cùng (bậc 2) -->
            <TextBox Margin="0,4,0,0">
                <local:BindingHelper.Rule>
                    <local:BindingConfig TargetPropertyName="Text"
                                         SourcePropertyName="Name"
                                         AncestorTypeName="StackPanel"
                                         AncestorDepth="2"/>
                </local:BindingHelper.Rule>
            </TextBox>

            <!-- Đồng bộ thanh trượt với chiều cao của Grid cha -->
            <Grid Height="40" Margin="0,4,0,0" Background="DodgerBlue">
                <Slider Minimum="10" Maximum="50" Width="450">
                    <local:BindingHelper.Rule>
                        <local:BindingConfig TargetPropertyName="Value"
                                             SourcePropertyName="Height"
                                             AncestorTypeName="Grid"/>
                    </local:BindingHelper.Rule>
                </Slider>
            </Grid>
        </StackPanel>
    </StackPanel>
</UserControl>

4. Ứng dụng trong DataTemplate và Controls lặp

Một trong những kịch bản thực tế nhất của kỹ thuật này nằm ở việc xử lý ràng buộc bên trong các thành phần lặp lại dữ liệu như ListBox hoặc ItemsControl. Khi sử dụng DataTemplate, các phần tử UI được sinh động bên trong vỏ bọc ListBoxItem. Do không thể đặt tên tĩnh cho các phần tử này, việc liên kết trực tiếp thuộc tính IsSelected của ListBoxItem với giao diện con trở nên khả thi thông qua tìm kiếm tổ tiên:

<ListBox Width="220">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid Width="200" Margin="2">
                <TextBlock Text="{Binding FullName}" VerticalAlignment="Center"/>
                <Ellipse Width="12" Height="12" Fill="Coral" HorizontalAlignment="Right">
                    <local:BindingHelper.Rule>
                        <local:BindingConfig TargetPropertyName="Visibility"
                                             SourcePropertyName="IsSelected"
                                             TransformConverter="{StaticResource VisConv}"
                                             AncestorTypeName="ListBoxItem"/>
                    </local:BindingHelper.Rule>
                </Ellipse>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Khi danh sách được render, mỗi dòng sẽ tạo ra một ListBoxItem chứa các phần tử từ mẫu dữ liệu. Ngay khi ellipse được tải, cơ chế đính kèm sẽ tự động leo ngược cây trực quan, định vị ListBoxItem đầu tiên gặp phải, và thiết lập mối liên kết hai chiều thông qua đối tượng trung gian. Phương pháp này loại bỏ hoàn toàn sự cần thiết của mã xử lý phía sau (code-behind) để theo dõi trạng thái chọn, đồng thời giữ cho giao diện hoàn toàn được điều khiển bởi khai báo XAML.

Thẻ: Silverlight XAML csharp AttachedProperty DataBinding

Đăng vào ngày 20 tháng 7 lúc 07:38