dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.02k stars 1.16k forks source link

WPF: Support custom RelativeSource on bindings #9408

Open vsfeedback opened 1 month ago

vsfeedback commented 1 month ago

This issue has been moved from a ticket on Developer Community.


I'm not sure if this is the proper place for a feature request for WPF, please redirect me if not.

I would like to be able to implement custom RelativeSource for bindings. This is not currently possible as the RelativeSource class only holds the configuration, while the actual resolution to a source object happens in RelativeObjectRef, which is sealed and internal.

My concrete use case is that I would like a RelativeSource that searches for the type of the DataContext, not the element itself.


Original Comments

Feedback Bot on 1/14/2022, 00:32 PM:

(private comment, text removed)

MichaeIDietrich commented 1 month ago

Here is a suggestion how you could do something similar with already available features:

Just add a custom markup extension that looks something like this:

namespace CustomBinding;

public class RelativeDataContextBinding : MarkupExtension
{
    public RelativeDataContextBinding(Type dataContextType)
    {
        DataContextType = dataContextType;
    }

    public PropertyPath Path { get; set; }

    public Type DataContextType { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (target is null)
            return DependencyProperty.UnsetValue;

        // is in template?
        if (target.TargetObject?.GetType().Name == "SharedDp")
            return this;

        var source = GetFrameworkElementByDataContext(target.TargetObject);

        return new Binding { Source = source, Path = Path, Mode = BindingMode.OneWay }.ProvideValue(serviceProvider);
    }

    private object? GetFrameworkElementByDataContext(object? referenceElement)
    {
        var current = referenceElement as DependencyObject;

        while (current is not null)
        {
            if (current is FrameworkElement element && element.DataContext?.GetType() == DataContextType)
                return element;

            current = VisualTreeHelper.GetParent(current);
        }

        return null;
    }
}

Here's a view model with child view models:

namespace CustomBinding;

public class MainViewModel
{
    public MainViewModel()
    {
        Items.Add(new ItemViewModel { Name = "Item 1" });
        Items.Add(new ItemViewModel { Name = "Item 2" });
        Items.Add(new ItemViewModel { Name = "Item 3" });
    }

    public ObservableCollection<ItemViewModel> Items { get; } = new();

    public int OtherProperty => 999;
}

public class ItemViewModel
{
    public required string Name { get; init; }
}

It could be used in XAML like this:

<Window x:Class="CustomBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CustomBinding">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <ListBox ItemsSource="{Binding Items}">
        <ListBox.ItemTemplate>
            <DataTemplate DataType="local:ItemViewModel">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Name, Mode=OneWay}" Margin="0,0,8,0" />
                    <TextBlock Text="{local:RelativeDataContextBinding local:MainViewModel, Path=DataContext.OtherProperty}" Foreground="Blue" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

Which then would result in this: image

This markup extension is just some idea how it could be done, it has several limitation like not handling DataContext changes. Hope this helps a bit.