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 codée qui ressemble à ceci:
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 de loin simplement configurer une liaison en XAML comme ceci:
<Canvas x:Name="designerCanvas"
Children="{Binding Document.Items}"
Width="{Binding Document.Width}"
Height="{Binding Document.Height}">
</Canvas>
Existe-t-il un moyen d'y parvenir sans recourir à une approche codée? 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 détruit mon Nice Model-View-ViewModel en rendant la vue consciente de son ViewModel.
Je ne pense pas qu'il soit possible d'utiliser une liaison avec la propriété des enfants. En fait, j'ai essayé de le faire aujourd'hui et cela m'a dérangé comme vous.
La toile est un contenant très rudimentaire. Il n'est vraiment pas conçu pour ce genre de travail. Vous devriez regarder dans l'un des nombreux ItemsControls. Vous pouvez lier la ObservableCollection de modèles de données de votre ViewModel à leur propriété ItemsSource et utiliser DataTemplates pour gérer la façon dont chacun des éléments est rendu dans le contrôle.
Si vous ne trouvez pas un ItemsControl qui rend vos éléments d'une manière satisfaisante, vous devrez peut-être créer un contrôle personnalisé qui fait ce dont vous avez besoin.
<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>
D'autres ont donné des réponses extensibles sur la façon de faire ce que vous voulez déjà faire. Je vais juste expliquer pourquoi vous ne pouviez 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 traitement spécial pour les collections. 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.
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-interface utilisateur.
Vous pouvez modéliser un ItemsControl
pour dessiner sur un Canvas
. L'idéal serait de définir le panneau de support sur Canvas
, puis de définir Canvas.Left
et Canvas.Top
propriétés sur les enfants immédiats. Je n'ai pas pu faire fonctionner cela parce que ItemsControl
encapsule 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 bac pour tous les éléments et les dessine chacun sur son 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 lequel j'ai utilisé pour configurer 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 exactement 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 de votre choix. Par exemple:
<ItemsControls ItemsSource="{Binding Document.Items}"><!--etc, etc...-->
Pour plus de détails sur ItemsControl et son fonctionnement, consultez ces documents: MSDN Library Reference ; modèle de données ; série du Dr WPF sur ItemsControl .
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}"/>