2009-05-20 10 views
54

Sono un po 'sorpreso dal fatto che non sia possibile impostare un'associazione per Canvas.Children tramite XAML. Ho dovuto ricorrere a un approccio code-behind che sembra qualcosa di simile:E 'possibile associare la proprietà Children di una Canvas in 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); 
} 

Preferirei mille volte appena istituito un binding in XAML come questo:

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

C'è un modo di realizzare questo senza ricorrere ad un approccio code-behind? Ho fatto alcuni googling sull'argomento, ma non ho escogitato molto per questo specifico problema.

Non mi piace il mio approccio attuale perché strappa il mio bel Model-View-ViewModel rendendo View consapevole del suo ViewModel.

risposta

8

Non credo sia possibile utilizzare il binding con la proprietà Children. In realtà ho provato a farlo oggi e ha commesso un errore su di me come hai fatto tu.

La tela è un contenitore molto rudimentale. In realtà non è progettato per questo tipo di lavoro. Dovresti esaminare uno dei tanti ItemsControls. È possibile associare la ObservableCollection ViewModel dei modelli di dati alla relativa proprietà ItemsSource e utilizzare DataTemplates per gestire il modo in cui ciascuno degli elementi viene visualizzato nel controllo.

Se non riesci a trovare un oggetto che esegue il rendering dei tuoi articoli in modo soddisfacente, potrebbe essere necessario creare un controllo personalizzato che faccia ciò che ti serve.

18

ItemsControl è progettato per creare raccolte dinamiche di controlli dell'interfaccia utente da altre raccolte, anche raccolte di dati non UI.

È possibile creare un modello con ItemsControl per disegnare su un Canvas. Il modo ideale consisterebbe nel settare il pannello di supporto su un Canvas e quindi impostare le proprietà Canvas.Left e Canvas.Top sui bambini immediati. Non ho potuto farlo funzionare perché ItemsControl avvolge i suoi figli con contenitori ed è difficile impostare le proprietà Canvas su questi contenitori.

Invece, io uso un Grid come un raccoglitore per tutti gli elementi e li disegna ognuno da soli Canvas. C'è un sovraccarico con questo approccio.

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

Ecco il codice dietro che ho usato per impostare la collezione fonte:

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 è una classe personalizzata che si comporta proprio come la versione System. L'ho creato per dimostrare che è possibile utilizzare le proprie classi personalizzate.

Un ultimo dettaglio: è possibile associare la proprietà ItemsSource a qualsiasi raccolta desiderata. Per esempio:

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

Per ulteriori dettagli su ItemsControl e come funziona, prova anche questi documenti: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.

+2

Questo non hanno problemi di prestazioni, dal momento che si sta aggiungendo una completamente nuova tela per ogni punto che si vuole disegnare? – 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

... Oppure puoi semplicemente impostare Canvas.Left e Canvas.Top sul contenitore che ho menzionato. Questa è la strada da percorrere. –

+13

difficilmente - la tua soluzione crea una singola tela per ogni oggetto - questa soluzione è molto più elegante ed efficiente in quanto viene utilizzata una sola tela per il rendering di tutti gli elementi – Xcalibur

+0

Una cosa che mi ha fatto cercare bug per 3 ore: {Binding Path = Circles} . Quando si esegue il binding a proprietà public class della finestra, si dovrebbe fare così: {Binding Path = Cerchi ElementName = $ WindowName $}. Altrimenti non funzionerà (almeno in VS 2012). – Nebril

24

Altri hanno dato risposte estensibili su come fare già ciò che si desidera effettivamente fare.Spiegherò solo perché non è possibile associare direttamente Children.

Il problema è molto semplice: l'obiettivo di associazione dati non può essere una proprietà di sola lettura e Panel.Children è di sola lettura. Non esiste una gestione speciale per le collezioni lì. Al contrario, ItemsControl.ItemsSource è una proprietà di lettura/scrittura, anche se è di tipo di raccolta - un evento raro per una classe .NET, ma richiesto in modo da supportare lo scenario di associazione.

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

E l'utilizzo di:

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