Можно ли связать свойство Canvas's Children в XAML? - PullRequest
57 голосов
/ 20 мая 2009

Я немного удивлен, что невозможно установить привязку для Canvas.Children через XAML. Мне пришлось прибегнуть к подходу с выделенным кодом, который выглядит примерно так:

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);
}

Я бы предпочел установить привязку в XAML следующим образом:

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

Есть ли способ сделать это, не прибегая к подходу с выделенным кодом? Я немного погуглил по этому вопросу, но мало что придумал для этой конкретной проблемы.

Мне не нравится мой текущий подход, потому что он портит мою красивую Model-View-ViewModel, заставляя View знать его ViewModel.

Ответы [ 5 ]

140 голосов
/ 23 июня 2009
<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>
25 голосов
/ 31 июля 2009

Другие дали расширенные ответы о том, как сделать то, что вы на самом деле хотите сделать. Я просто объясню, почему вы не можете связать Children напрямую.

Проблема очень проста - цель привязки данных не может быть свойством только для чтения, а Panel.Children только для чтения. Там нет специальной обработки для коллекций там. Напротив, ItemsControl.ItemsSource является свойством чтения / записи, даже если оно имеет тип коллекции - редкий случай для класса .NET, но требуется для поддержки сценария привязки.

19 голосов
/ 21 мая 2009

ItemsControl предназначен для создания динамических коллекций элементов управления пользовательского интерфейса из других коллекций, даже коллекций данных, не относящихся к пользовательскому интерфейсу.

Вы можете создать шаблон ItemsControl для рисования на Canvas. Идеальным способом было бы установить заднюю панель на Canvas, а затем установить свойства Canvas.Left и Canvas.Top для ближайших потомков. Я не мог заставить это работать, потому что ItemsControl оборачивает его дочерние элементы контейнерами, и трудно установить свойства Canvas для этих контейнеров.

Вместо этого я использую Grid в качестве корзины для всех предметов и рисую их по отдельности 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>

Вот код, который я использовал для настройки исходной коллекции:

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;

MyPoint - это пользовательский класс, который ведет себя так же, как System версия. Я создал его, чтобы продемонстрировать, что вы можете использовать свои собственные классы.

Одна последняя деталь: вы можете привязать свойство ItemsSource к любой коллекции, которую вы хотите. Например:

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

Для получения дополнительной информации о ItemsControl и о том, как он работает, ознакомьтесь со следующими документами: Справочник по библиотеке MSDN ; Шаблонирование данных ; Серия доктора WPF на ItemsControl .

12 голосов
/ 20 сентября 2010
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);
        }
    }
}

И используя:

<Canvas x:Name="PART_SomeCanvas"
        Controls:CanvasAssistant.BoundChildren="{TemplateBinding SomeItems}"/>
9 голосов
/ 21 мая 2009

Я не считаю возможным использовать связывание со свойством Children. Я действительно пытался сделать это сегодня, и это дало мне ошибку, как и тебе.

Canvas - очень элементарный контейнер. Он действительно не предназначен для такой работы. Вы должны заглянуть в один из множества ItemsControls. Вы можете привязать ObservableCollection вашей ViewModel моделей данных к их свойству ItemsSource и использовать DataTemplates для обработки того, как каждый из элементов отображается в элементе управления.

Если вы не можете найти ItemsControl, который визуализирует ваши элементы удовлетворительным образом, вам, возможно, придется создать пользовательский элемент управления, который делает то, что вам нужно.

...