2009-05-20 9 views
54

Je suis un peu surpris qu'il ne soit pas possible de configurer une liaison pour Canvas.Children via XAML. J'ai dû recourir à une approche code-behind qui ressemble à ceci:Est-il possible de lier une propriété Canvas Children dans 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); 
} 

Je préfère simplement mettre en place une reliure en XAML comme ceci:

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

est-il un manière d'accomplir ceci sans recourir à une approche de code-behind? J'ai fait quelques recherches sur le sujet, mais je n'ai pas trouvé grand chose pour ce problème spécifique.

Je n'aime pas mon approche actuelle, car elle corrige ma belle Model-View-ViewModel en la rendant visible par ViewModel.

Répondre

8

Je ne crois pas qu'il soit possible d'utiliser la liaison avec la propriété Children. En fait, j'ai essayé de le faire aujourd'hui et ça m'a fait mal comme vous l'avez fait.

Le Canvas est un conteneur très rudimentaire. Ce n'est vraiment pas conçu pour ce genre de travail. Vous devriez regarder dans l'un des nombreux ItemsControls. Vous pouvez lier ObservableCollection de modèles de données de votre ViewModel à leur propriété ItemsSource et utiliser DataTemplates pour gérer la façon dont chaque élément est rendu dans le contrôle.

Si vous ne trouvez pas de ItemsControl qui restitue vos éléments de manière satisfaisante, vous devrez peut-être créer un contrôle personnalisé qui répondra à vos besoins.

18

ItemsControl est conçu pour créer des collections dynamiques de contrôles d'interface utilisateur à partir d'autres collections, même des collections de données non-UI. Vous pouvez modéliser un ItemsControl pour dessiner sur un Canvas. Le moyen idéal serait de définir le panneau de sauvegarde à Canvas puis de définir les propriétés Canvas.Left et Canvas.Top sur les enfants immédiats. Je ne pouvais pas obtenir ce travail parce que ItemsControl enveloppe ses enfants avec des conteneurs et il est difficile de définir les propriétés Canvas sur ces conteneurs.

Au lieu de cela, j'utilise un Grid comme un bac pour tous les éléments et les dessine chacun sur leur propre Canvas. Il y a des frais généraux avec cette approche.

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

Voici le code derrière que je l'habitude de mettre en place la collection source:

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 est une classe personnalisée qui se comporte comme la version System. Je l'ai créé pour démontrer que vous pouvez utiliser vos propres classes personnalisées.

Un dernier détail: Vous pouvez lier la propriété ItemsSource à n'importe quelle collection. Par exemple:

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

Pour plus de détails sur ItemsControl et comment il fonctionne, consultez ces documents: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.

+2

Est-ce pas ont des problèmes de performance, puisque vous ajoutez une toile entièrement nouvelle pour chaque point que vous voulez dessiner? – 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

... Ou vous pouvez simplement définir Canvas.Left et Canvas.Top sur le conteneur que j'ai mentionné. C'est le chemin à parcourir. –

+13

difficilement - votre solution crée une toile individuelle pour chaque article - cette solution est beaucoup plus élégante et efficace car une seule toile est utilisée pour rendre tous les éléments – Xcalibur

+0

Une chose qui m'a fait chercher des bogues pendant 3 heures: {Binding Path = Cercles} . Lorsque vous vous liez à la propriété publique de la classe window, vous devriez le faire comme ceci: {Binding Path = Circles ElementName = $ WindowName $}. Sinon, ça ne marchera pas (au moins dans VS 2012). – Nebril

24

D'autres ont donné des réponses extensibles sur la façon de faire ce que vous voulez réellement faire déjà.Je vais juste expliquer pourquoi vous ne pouvez pas lier directement Children.

Le problème est très simple: la cible de liaison de données ne peut pas être une propriété en lecture seule et Panel.Children est en lecture seule. Il n'y a pas de manipulation spéciale pour les collections là-bas. En revanche, ItemsControl.ItemsSource est une propriété en lecture/écriture, même si elle est de type collection - une occurrence rare pour une classe .NET, mais nécessaire pour prendre en charge le scénario de liaison.

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

Et en utilisant:

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