2009-05-20 5 views
54

Ich bin ein wenig überrascht, dass es nicht möglich ist, eine Bindung für Canvas.Children über XAML einzurichten. Ich habe auf einen Code-behind-Ansatz zurückgreifen, die etwa wie folgt aussieht:Ist es möglich, die Canvas-Eigenschaft eines Canvas in XAML zu binden?

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

Ich würde viel lieber nur eine Bindung in XAML wie folgt aufgebaut:

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

Gibt es eine Um dies zu erreichen, ohne auf einen Code-Behind-Ansatz zurückzugreifen? Ich habe etwas über das Thema gegoogelt, aber ich habe mir für dieses spezielle Problem nicht viel einfallen lassen.

Ich mag meinen derzeitigen Ansatz nicht, weil er mein schönes Model-View-ViewModel muckt, indem ich das View auf sein ViewModel aufmerksam mache.

Antwort

8

Ich glaube nicht, dass es möglich ist, Bindung mit dem Children Eigentum zu verwenden. Ich habe das heute tatsächlich versucht und es hat sich an mir verändert, so wie du es getan hast.

Der Canvas ist ein sehr rudimentärer Container. Es ist wirklich nicht für diese Art von Arbeit konzipiert. Sie sollten in eines der vielen ItemsControls schauen. Sie können die ObservableCollection von ViewModel von Datenmodellen an ihre ItemsSource-Eigenschaft binden und mithilfe von DataTemplates verarbeiten, wie die einzelnen Elemente im Steuerelement gerendert werden.

Wenn Sie ein ItemsControl nicht finden können, das Ihre Elemente auf eine zufriedenstellende Weise rendert, müssen Sie möglicherweise ein benutzerdefiniertes Steuerelement erstellen, das das ausführt, was Sie benötigen.

18

ItemsControl ist für die Erstellung dynamischer Sammlungen von UI-Steuerelementen aus anderen Sammlungen, auch Nicht-UI-Datensammlungen, konzipiert.

Sie können Vorlage ItemsControl auf eine Canvas zeichnen. Der ideale Weg würde darin bestehen, die Trägerplatte auf Canvas einzustellen und dann die Eigenschaften Canvas.Left und Canvas.Top auf die unmittelbaren Kinder zu setzen. Ich konnte das nicht zum Laufen bringen, weil ItemsControl seine Kinder mit Behältern umwickelt und es ist schwer, die Canvas Eigenschaften auf diesen Behältern einzustellen.

Stattdessen verwende ich einen Grid als einen Behälter für alle Elemente und zeichne sie jeweils auf eigene Canvas. Es gibt einige Overhead mit diesem Ansatz.

<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> 

Hier ist der Code-behind, dass ich verwendet, um die Quellensammlung einzurichten:

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 ist eine benutzerdefinierte Klasse, die genau wie die System Version verhält. Ich habe es erstellt, um zu demonstrieren, dass Sie Ihre eigenen benutzerdefinierten Klassen verwenden können.

Ein letztes Detail: Sie können die ItemsSource-Eigenschaft an jede beliebige Auflistung binden. Zum Beispiel:

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

Für weitere Details über Items und wie es funktioniert, diese Dokumente überprüfen: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.

+2

Ist dies nicht Performance-Probleme haben, da Sie eine völlig neue Leinwand für jeden Punkt sind das Hinzufügen Sie zeichnen möchten? – Benjamin

137
<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> 
+0

... Oder Sie können Canvas.Left und Canvas.Top nur auf den Container setzen, den ich erwähnt habe. Dies ist der Weg zu gehen. –

+13

kaum - Ihre Lösung erstellt eine individuelle Leinwand für jedes Element - diese Lösung ist viel eleganter und effizienter, da nur eine Leinwand verwendet wird, um alle Objekte zu rendern – Xcalibur

+0

Eine Sache, die mich 3 Stunden lang nach Fehlern suchte: {Binding Path = Circles} . Wenn Sie an die öffentliche Eigenschaft der Fensterklasse binden, sollten Sie Folgendes tun: {Binding Path = Circles ElementName = $ WindowName $}. Ansonsten wird es einfach nicht funktionieren (zumindest in VS 2012). – Nebril

24

Andere haben erweiterbare Antworten gegeben, wie Sie tun können, was Sie eigentlich schon tun möchten.Ich werde nur erklären, warum Sie Children nicht direkt binden können.

Das Problem ist sehr einfach - Datenbindung Ziel kann keine schreibgeschützte Eigenschaft sein, und Panel.Children ist schreibgeschützt. Dort gibt es keine spezielle Behandlung für Sammlungen. Im Gegensatz dazu ist ItemsControl.ItemsSource eine Lese-/Schreibeigenschaft, obwohl sie vom Auflistungstyp ist - ein seltenes Vorkommen für eine .NET-Klasse, aber erforderlich, um das Bindungsszenario zu unterstützen.

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

Und mit:

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