2009-05-20 18 views
54

Estoy un poco sorprendido de que no sea posible configurar un enlace para Canvas.Children a través de XAML. He tenido que recurrir a un enfoque de código subyacente que se ve algo como esto:¿Es posible enlazar una propiedad de Canvas's Children en 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); 
} 

Prefiero acaba de crear una unión en XAML como esto:

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

¿Existe una forma de lograr esto sin recurrir a un enfoque de código subyacente? He hecho algunas búsquedas en Google sobre el tema, pero no he encontrado mucho para este problema específico.

no me gusta mi enfoque actual porque mucks mi buen Model-View-ViewModel al hacer la vista consciente de que es modelo de vista.

Respuesta

8

No creo que sea posible utilizar el enlace con la propiedad Children. De hecho, traté de hacer eso hoy y tuve un error en mí al igual que tú.

El lienzo es un contenedor muy rudimentario. Realmente no está diseñado para este tipo de trabajo. Debería ver uno de los muchos controles de elementos. Puede vincular su ObservableCollection de modelos de datos de ViewModel a su propiedad ItemsSource y usar DataTemplates para controlar cómo se representan cada uno de los elementos en el control.

Si no puede encontrar una ItemsControl que hace que sus artículos de manera satisfactoria, es posible que tenga que crear un control personalizado que hace lo que necesita.

18

ItemsControl está diseñado para crear colecciones dinámicas de los controles de interfaz de usuario de otras colecciones, incluso colecciones de datos no de interfaz de usuario.

Puede modelar un ItemsControl para dibujar en un Canvas. La forma ideal sería establecer el panel de respaldo en Canvas y luego establecer las propiedades Canvas.Left y Canvas.Top en los elementos secundarios inmediatos. No pude hacer que esto funcione porque ItemsControl envuelve a sus hijos con contenedores y es difícil establecer las propiedades Canvas en estos contenedores.

En su lugar, utilizo un Grid como un contenedor para todos los elementos y los dibujo cada uno en su propio Canvas. Hay algunos gastos generales con este enfoque.

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

Aquí está el código subyacente que utiliza para configurar la colección de origen:

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 es una clase personalizada que se comporta igual que la versión System. Lo creé para demostrar que puedes usar tus propias clases personalizadas.

Un último detalle: puede enlazar la propiedad ItemsSource a cualquier colección que desee. Por ejemplo:

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

Para más detalles acerca de ItemsControl y cómo funciona, echa un vistazo a estos documentos: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.

+2

¿Esto no tiene problemas de rendimiento, ya que se va a añadir una nueva tela para cada punto que desea dibujar? – 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

... O simplemente podría establecer Canvas.Left y Canvas.Top en el contenedor que mencioné. Este es el camino a seguir. –

+13

difícilmente - su solución crea un lienzo individual para cada elemento - esta solución es mucho más elegante y eficiente ya que solo se utiliza un lienzo para representar todos los elementos – Xcalibur

+0

Una cosa que me hizo buscar errores durante 3 horas: {Binding Path = Circles} . Cuando se vincula a la propiedad pública de la clase de ventana, debe hacer esto así: {Binding Path = Circles ElementName = $ WindowName $}. De lo contrario, simplemente no funcionará (al menos en VS 2012). – Nebril

24

Otros han dado respuestas extensibles sobre cómo hacer lo que realmente desea hacer.Voy a explicar por qué no puedes unir Children directamente.

El problema es muy simple: el destino de enlace de datos no puede ser una propiedad de solo lectura, y Panel.Children es de solo lectura. No hay un manejo especial para las colecciones allí. Por el contrario, ItemsControl.ItemsSource es una propiedad de lectura/escritura, aunque es de tipo de recopilación, una ocurrencia rara para una clase .NET, pero necesaria para admitir el escenario vinculante.

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

Y usando:

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