C# 是否可以在 XAML 中绑定 Canvas 的 Children 属性?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/889825/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-05 08:35:21  来源:igfitidea点击:

Is it possible to bind a Canvas's Children property in XAML?

c#wpfdata-binding.net-3.5canvas

提问by Rob

I'm a little surprised that it is not possible to set up a binding for Canvas.Children through XAML. I've had to resort to a code-behind approach that looks something like this:

我有点惊讶无法通过 XAML 为 Canvas.Children 设置绑定。我不得不求助于一种看起来像这样的代码隐藏方法:

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
    DesignerViewModel dvm = this.DataContext as DesignerViewModel;
    dvm.Document.Items.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Items_CollectionChanged);

    foreach (UIElement element in dvm.Document.Items)
        designerCanvas.Children.Add(element);
}

private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    ObservableCollection<UIElement> collection = sender as ObservableCollection<UIElement>;

    foreach (UIElement element in collection)
        if (!designerCanvas.Children.Contains(element))
            designerCanvas.Children.Add(element);

    List<UIElement> removeList = new List<UIElement>();
    foreach (UIElement element in designerCanvas.Children)
        if (!collection.Contains(element))
            removeList.Add(element);

    foreach (UIElement element in removeList)
        designerCanvas.Children.Remove(element);
}

I'd much rather just set up a binding in XAML like this:

我宁愿像这样在 XAML 中设置一个绑定:

<Canvas x:Name="designerCanvas"
        Children="{Binding Document.Items}"
        Width="{Binding Document.Width}"
        Height="{Binding Document.Height}">
</Canvas>

Is there a way to accomplish this without resorting to a code-behind approach? I've done some googling on the subject, but haven't come up with much for this specific problem.

有没有办法在不诉诸代码隐藏方法的情况下实现这一目标?我已经对这个主题进行了一些谷歌搜索,但没有针对这个特定问题提出太多建议。

I don't like my current approach because it mucks up my nice Model-View-ViewModel by making the View aware of it's ViewModel.

我不喜欢我目前的方法,因为它通过让 View 知道它是 ViewModel 来破坏我漂亮的 Model-View-ViewModel。

采纳答案by Rob

I don't believe its possible to use binding with the Children property. I actually tried to do that today and it errored on me like it did you.

我不相信它可以与 Children 属性一起使用。我今天实际上尝试这样做,但它像你一样出错。

The Canvas is a very rudimentary container. It really isn't designed for this kind of work. You should look into one of the many ItemsControls. You can bind your ViewModel's ObservableCollection of data models to their ItemsSource property and use DataTemplates to handle how each of the items is rendered in the control.

Canvas 是一个非常初级的容器。它真的不是为这种工作而设计的。您应该查看众多 ItemsControl 之一。您可以将数据模型的 ViewModel 的 ObservableCollection 绑定到它们的 ItemsSource 属性,并使用 DataTemplates 来处理每个项目在控件中的呈现方式。

If you can't find an ItemsControl that renders your items in a satisfactory way, you might have to create a custom control that does what you need.

如果您找不到以令人满意的方式呈现您的项目的 ItemsControl,您可能必须创建一个自定义控件来满足您的需求。

回答by Josh G

ItemsControlis designed for creating dynamic collections of UI controls from other collections, even non-UI data collections.

ItemsControl旨在从其他集合(甚至非 UI 数据集合)创建 UI 控件的动态集合。

You can template an ItemsControlto draw on a Canvas. The ideal way would involve setting the backing panel to a Canvasand then setting the Canvas.Leftand Canvas.Topproperties on the immediate children. I could not get this to work because ItemsControlwraps its children with containers and it is hard to set the Canvasproperties on these containers.

您可以模板 anItemsControl以在Canvas. 理想的方法是将支持面板设置为 a Canvas,然后在直接子项上设置Canvas.LeftCanvas.Top属性。我无法ItemsControl让它工作,因为用容器包装它的孩子,并且很难Canvas在这些容器上设置属性。

Instead, I use a Gridas a bin for all of the items and draw them each on their own Canvas. There is some overhead with this approach.

相反,我使用 aGrid作为所有项目的垃圾箱,并单独绘制它们Canvas。这种方法有一些开销。

<ItemsControl x:Name="Collection" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type local:MyPoint}">
            <Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <Ellipse Width="10" Height="10" Fill="Black" Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}"/>
            </Canvas>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Here's the code behind that I used to set up the source collection:

这是我用来设置源集合的代码:

List<MyPoint> points = new List<MyPoint>();

points.Add(new MyPoint(2, 100));
points.Add(new MyPoint(50, 20));
points.Add(new MyPoint(200, 200));
points.Add(new MyPoint(300, 370));

Collection.ItemsSource = points;

MyPointis a custom class that behaves just like the Systemversion. I created it to demonstrate that you can use your own custom classes.

MyPoint是一个行为就像System版本一样的自定义类。我创建它是为了证明您可以使用自己的自定义类。

One final detail: You can bind the ItemsSource property to any collection you want. For example:

最后一个细节:您可以将 ItemsSource 属性绑定到您想要的任何集合。例如:

<ItemsControls ItemsSource="{Binding Document.Items}"><!--etc, etc...-->

For further details about ItemsControl and how it works, check out these documents: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.

有关 ItemsControl 及其工作原理的更多详细信息,请查看以下文档:MSDN 库参考数据模板WPF 博士关于 ItemsControl 的系列

回答by kenwarner

<ItemsControl ItemsSource="{Binding Path=Circles}">
    <ItemsControl.ItemsPanel>
         <ItemsPanelTemplate>
              <Canvas Background="White" Width="500" Height="500"  />
         </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Ellipse Fill="{Binding Path=Color, Converter={StaticResource colorBrushConverter}}" Width="25" Height="25" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
            <Setter Property="Canvas.Left" Value="{Binding Path=X}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

回答by Pavel Minaev

Others have given extensible replies on how to do what you actually want to do already. I'll just explain why you couldn't bind Childrendirectly.

其他人已经就如何做你真正想做的事情给出了可扩展的答复。我将解释为什么你不能Children直接绑定。

The problem is very simple - data binding target cannot be a read-only property, and Panel.Childrenis read-only. There is no special handling for collections there. In contrast, ItemsControl.ItemsSourceis a read/write property, even though it is of collection type - a rare occurence for a .NET class, but required so as to support the binding scenario.

问题很简单——数据绑定目标不能是只读属性,而且Panel.Children是只读的。那里没有对集合的特殊处理。相比之下,ItemsControl.ItemsSource是读/写属性,即使它是集合类型 - .NET 类很少出现,但需要以支持绑定方案。

回答by Ivan Shikht

internal static class CanvasAssistant
{
    #region Dependency Properties

    public static readonly DependencyProperty BoundChildrenProperty =
        DependencyProperty.RegisterAttached("BoundChildren", typeof (object), typeof (CanvasAssistant),
                                            new FrameworkPropertyMetadata(null, onBoundChildrenChanged));

    #endregion

    public static void SetBoundChildren(DependencyObject dependencyObject, string value)
    {
        dependencyObject.SetValue(BoundChildrenProperty, value);
    }

    private static void onBoundChildrenChanged(DependencyObject dependencyObject,
                                               DependencyPropertyChangedEventArgs e)
    {
        if (dependencyObject == null)
        {
            return;
        }
        var canvas = dependencyObject as Canvas;
        if (canvas == null) return;

        var objects = (ObservableCollection<UIElement>) e.NewValue;

        if (objects == null)
        {
            canvas.Children.Clear();
            return;
        }

        //TODO: Create Method for that.
        objects.CollectionChanged += (sender, args) =>
                                            {
                                                if (args.Action == NotifyCollectionChangedAction.Add)
                                                    foreach (object item in args.NewItems)
                                                    {
                                                        canvas.Children.Add((UIElement) item);
                                                    }
                                                if (args.Action == NotifyCollectionChangedAction.Remove)
                                                    foreach (object item in args.OldItems)
                                                    {
                                                        canvas.Children.Remove((UIElement) item);
                                                    }
                                            };

        foreach (UIElement item in objects)
        {
            canvas.Children.Add(item);
        }
    }
}

And using:

并使用:

<Canvas x:Name="PART_SomeCanvas"
        Controls:CanvasAssistant.BoundChildren="{TemplateBinding SomeItems}"/>