I'm currently using the MVVM pattern in one of my apps, to be more specific I'm using the MVVMLight framework. In one of the pages, I will have a screen where the user can input the width and length to draw rectangles, there is not much code logic so, I was thinking to put all of my code in the code-behind since most of what will be happening in this screen is UI related.
Does that make sense to use the code-behind in this case? If not, how would you structure the code to use the MVVM pattern, what would you put in the ViewModel in this case and what would you put in your code behind?
Here is the code without using MVVM.
XAML:
<Window x:Class="DrawingRectangles.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DrawingRectangles"
mc:Ignorable="d"
Title="MainWindow" Height="531.798" Width="782.115">
<Grid Name="MyGrid" Width="480" Height="240" Margin="27,23,267,174">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="59*"/>
<ColumnDefinition Width="421*"/>
</Grid.ColumnDefinitions>
<Canvas Name="MyCanvas" Background="#FFF1F0F0" Margin="10" Grid.ColumnSpan="2"/>
<Grid Margin="10,235,10,-92" Background="WhiteSmoke" Grid.ColumnSpan="2">
<Button x:Name="drawButton" Content="Draw" Click="drawButton_Click"/>
<Button x:Name="resetButton" Content="Reset" Click="resetButton_Click"/>
<TextBox x:Name="textBoxPartWidth"/>
<TextBox x:Name="textBoxPartLength"/>
</Grid>
</Grid>
</Window>
CODE-BEHIND:
namespace DrawingRectangles
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void drawButton_Click(object sender, RoutedEventArgs e)
{
clearScreen();
int xParts = 10;
int yParts = 10;
for (int i = 0; i < xParts; i++) {
for (int j = 0; j < yParts; j++) {
// Create a rectangle.
Rectangle myRectangle = new Rectangle();
myRectangle.Width = Convert.ToDouble(textBoxPartLength.Text);
myRectangle.Height = Convert.ToDouble(textBoxPartWidth.Text);
myRectangle.Margin = new Thickness((Convert.ToInt32(myRectangle.Width) + 1) * i, (Convert.ToInt32(myRectangle.Height) + 1) * j, 0, 0);
myRectangle.Fill = new SolidColorBrush(Color.FromArgb(170, 51, 51, 255));
MyCanvas.Children.Add(myRectangle);
}
}
}
private void resetButton_Click(object sender, RoutedEventArgs e)
{
MyCanvas.Children.Clear();
}
private void clearScreen()
{
MyCanvas.Children.Clear();
}
}
}
UI
EDIT:
Second Image (reference only):
The Button in the view should be bound to an ICommand property of the view model. The command will be executed when you click on the Button. Please refer to this blog post for information about how to handle events in an MVVM application. In MvvmLight, the ICommand implementation is called RelayCommand.
You should also bind the Text properties of the TextBoxes to two source properties of the view model and the Canvas element in your view should be replaced with an ItemsControl that you bind to a collection of objects that are defined in the view model.
Please refer to the following sample code.
Model:
public class Model
{
public int Width { get; set; }
public int Height { get; set; }
public Thickness Margin { get; set; }
public Brush Fill { get; set; }
}
View:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="#FFF1F0F0" Margin="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Width="{Binding Width}"
Height="{Binding Height}"
Margin="{Binding Margin}"
Fill="{Binding Fill}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Draw" Command="{Binding DrawCommand}" />
<Button Content="Reset" Command="{Binding ResetCommand}" />
<TextBox Text="{Binding Width}"/>
<TextBox Text="{Binding Height}"/>
View Model:
public class ViewModel
{
public ViewModel()
{
DrawCommand = new RelayCommand(Draw);
ResetCommand = new RelayCommand(Clear);
}
public ObservableCollection<Model> Items { get; } = new ObservableCollection<Model>();
public RelayCommand DrawCommand { get; }
public RelayCommand ResetCommand { get; }
public int Width { get; set; }
public int Height { get; set; }
private void Draw()
{
Clear();
int xParts = 10;
int yParts = 10;
for (int i = 0; i < xParts; i++)
{
for (int j = 0; j < yParts; j++)
{
Model model = new Model();
model.Width = Width;
model.Height = Height;
model.Margin = new Thickness((model.Width + 1) * i, (model.Height + 1) * j, 0, 0);
model.Fill = new SolidColorBrush(Color.FromArgb(170, 51, 51, 255));
Items.Add(model);
}
}
}
private void Clear()
{
Items.Clear();
}
}
In this example all application logic has been moved to the view model where it belongs. There is no logic left in the code-behind class of the view.
Also note that the view models creates instances of Model objects rather than creating Rectangle elements. It's generally considered be a bad practice to reference UI elements in a view model class. The Rectangle elements are created by the ItemsControl. See the ItemTemplate in the view.
Related
The following code draws rectangles in 2D grid. Everything is working fine except it is really slow when it has to draw more than 50,000 squares. I know it sounds like a lot of squares but I have the same program written in C++/Qt and it's a lot faster, it draws the 50,000 almost instantaneously wheres in C#/WPF it takes 50 seconds.
Is there a better/faster way to draw rectangles on the screen in WPF?
XAML
<Window x:Class="DrawingRectanglesWithMvvmLight.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ignore="http://www.galasoft.ch/ignore"
mc:Ignorable="d ignore"
Height="319"
Width="453.333"
Title="MVVM Light Application"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Skins/MainSkin.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid x:Name="LayoutRoot" Margin="0,0,2,0" Height="194" VerticalAlignment="Top" Background="#FF3E7AAC">
<ItemsControl ItemsSource="{Binding PartsGrid}" Height="200" Margin="5,0,10,-6">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="#FFF1F0F0" Margin="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle
Width="{Binding Width}"
Height="{Binding Height}"
Margin="{Binding Margin}"
Fill="{Binding Fill}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid Margin="5,236,6,-69">
<Button Content="Draw" Command="{Binding DrawCommand}" Margin="328,0,10,0" />
<Button Content="Reset" Command="{Binding ResetCommand}" Margin="263,0,109,0" />
<TextBox Text="{Binding Width}" RenderTransformOrigin="1.049,2.023" Margin="124,0,200,0"/>
<TextBox Text="{Binding Height}" Margin="0,0,334,0"/>
</Grid>
</Grid>
</Window>
Class:
namespace MvvmLightTEST.Model
{
public class FSRectangle
{
public double Width { get; set; }
public double Height { get; set; }
public Thickness Margin { get; set; }
public Brush Fill { get; set; }
}
}
ViewModel:
namespace DrawingRectanglesWithMvvmLight.ViewModel
{
public class MainViewModel : ViewModelBase
{
public ObservableCollection<FSRectangle> PartsGrid { get; } = new ObservableCollection<FSRectangle>();
public RelayCommand DrawCommand { get; }
public RelayCommand ResetCommand { get; }
public double Width { get; set; }
public double Height { get; set; }
public MainViewModel(IDataService dataService)
{
DrawCommand = new RelayCommand(Draw);
ResetCommand = new RelayCommand(Clear);
}
private void Draw()
{
Clear();
int xParts = 250;
int yParts = 200;
for (int i = 0; i < xParts; i++) {
for (int j = 0; j < yParts; j++) {
FSRectangle part = new FSRectangle();
part.Width = Width;
part.Height = Height;
part.Margin = new Thickness((part.Width + 1) * i, (part.Height + 1) * j, 0, 0);
part.Fill = new SolidColorBrush(Color.FromArgb(170, 51, 51, 255));
PartsGrid.Add(part);
}
}
}
private void Clear()
{
PartsGrid.Clear();
}
}
}
UI
By not using MVVM due to the slow performance for this use case. Rectangles are FrameworkElements which contain layout, a feature that does not scale This is discussed many times in SO.
You might want to consider
using DrawingVisual and ContainerVisual for lower level rendering with no layout overhead
no MVVM
create render visuals ahead of time rather than per render
Image courtesy Microsoft, used without permission.
MSDN has this to say on DrawingVisual
The DrawingVisual is a lightweight drawing class that is used to render shapes, images, or text. This class is considered lightweight because it does not provide layout or event handling, which improves its runtime performance. For this reason, drawings are ideal for backgrounds and clip art. The DrawingVisual can be used to create a custom visual object.
See also
WPF Graphics Rendering Overview
Path with RectangleGeometry is an option:
https://learn.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/how-to-define-a-rectangle-using-a-rectanglegeometry
If I have an ObservableCollection in one of my classes. In my code behind my view I have an object of this class and use it as the DataBinding
this.DataContext = MyCustomClass;
in the xaml code of the view I want to bind several buttons to items in the Observable collection. Something like this:
<Button x:Name="Bid_Price_10" Grid.Row="0" Content="{Binding myObservableCollection[0].Price, Mode=OneWay}" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" UseLayoutRounding="True" Padding="0"/>
<Button x:Name="Bid_Price_11" Grid.Row="1" Content="{Binding myObservableCollection[1].Price, Mode=OneWay}" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" UseLayoutRounding="True" Padding="0"/>
At the moment this is not working, am I missing something ?
EDIT: Create full code to demo what I am trying to do:
So I have a Coffee class:
class Coffee
{
public int price { get; set; }
}
I have a drinks class that holds a list of coffees:
class Drinks
{
public List<Coffee> CoffeeList;
public Drinks()
{
CoffeeList = new List<Coffee>();
for (int i = 0; i < 10; i++)
{
Coffee c = new Coffee();
c.price = i;
CoffeeList.Add(c);
}
this.startCoffeePriceUpdateThread();
}
private void UpdateCofffeePrice()
{
while (true)
{
Thread.Sleep(1000);
foreach (var c in CoffeeList)
{
c.price++;
}
}
}
public void startCoffeePriceUpdateThread()
{
Thread coffeeThread = new Thread(new ThreadStart(UpdateCofffeePrice));
coffeeThread.Start();
}
}
my main window code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Drinks ourDrinks = new Drinks();
this.DataContext = ourDrinks;
}
}
and my xaml code:
<Grid x:Name="Grid" HorizontalAlignment="Left" Height="193" Margin="77,31,0,0" VerticalAlignment="Top" Width="315">
<Button x:Name="Button" Content="{Binding CoffeeList[0].Price}" HorizontalAlignment="Left" Margin="49,25,0,0" VerticalAlignment="Top" Width="75" Height="28"/>
</Grid>
So the problem is that I am not seeing anything in the button. At the moment I am not using INotifyPropertyChange as I have been advised it would not be needed.
As Clemens suggested: if everything is fixed, you won't need ObservableCollection nor PropertyChanged notification. The following code should be sufficient:
public class Stuff
{
public string Price { get; set; }
}
public class myCustomClass
{
public List<Stuff> myCollection { get; set; }
}
public partial class MainWindow : Window
{
public myCustomClass myCustomInstance = new myCustomClass();
public MainWindow()
{
InitializeComponent();
myCustomInstance.myCollection = new List<Stuff>();
myCustomInstance.myCollection.Add(new Stuff() { Price = "1200$" });
myCustomInstance.myCollection.Add(new Stuff() { Price = "5.5$" });
this.DataContext = myCustomInstance;
}
}
And the simplified XAML
<Button Content="{Binding myCollection[0].Price, Mode=OneWay}"/>
<Button Content="{Binding myCollection[1].Price, Mode=OneWay}" />
NB: I specified an instance of myCustomClass as the DataContext. If you really need to bind it to the class, let me know and I will update the code.
I have two ListBoxs defined in my XAML and one Class MyListItem.
Now one ListBox should display the name as button and the second ListBox should display the name as a TextBlock.
Here a little example, both ListBoxs behave the same.
MyListItem
public class MyListItem
{
private string _name;
public string Name
{
get{return _name;}
set{_name = value;}
}
}
XAML
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DataTemplate.Views.MainWindow"
xmlns:viewsmodels="clr-namespace:DataTemplate.ViewModels;assembly=DataTemplate"
xmlns:dt="clr-namespace:DataTemplate;assembly=DataTemplate"
Title="DataTemplate" Width="700">
<Window.DataContext>
<viewsmodels:MainWindowViewModel />
</Window.DataContext>
<Grid ColumnDefinitions="250,250,250">
<ItemsControl Grid.Column="1" Items="{Binding List2}">
<ItemsControl.DataTemplates>
<DataTemplate DataType="{x:Type dt:MyListItem}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
<ItemsControl Grid.Column="2" Items="{Binding List3}">
<ItemsControl.DataTemplates>
<DataTemplate DataType="{x:Type dt:MyListItem}">
<Button Content="{Binding Name}"/>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Grid>
</Window>
ViewMode
public class MainWindowViewModel
{
public ObservableCollection<MyListItem> List1 { get; set; }
public ObservableCollection<MyListItem> List2 { get; set; }
public ObservableCollection<MyListItem> List3 { get; set; }
public MainWindowViewModel()
{
List1 = new ObservableCollection<MyListItem>();
List2 = new ObservableCollection<MyListItem>();
List3 = new ObservableCollection<MyListItem>();
Random rand = new Random();
for (int i = 0; i < rand.Next(1, 20); i++)
{
MyListItem mli = new MyListItem();
mli.Name = "ListItem" + i;
List1.Add(mli);
}
for (int i = 0; i < rand.Next(1, 20); i++)
{
MyListItem mli = new MyListItem();
mli.Name = "ListItem" + i;
List2.Add(mli);
}
for (int i = 0; i < rand.Next(1, 20); i++)
{
MyListItem mli = new MyListItem();
mli.Name = "ListItem" + i;
List3.Add(mli);
}
}
}
Unfortunately there's currently no good way to do this in Avalonia that I can think of. The most obvious way would be to add the data templates to a <Style.Resources> collection and use {StyleResource} to reference them, but this doesn't work currently.
I think you have two alternatives here for the moment:
Just copy and paste the data templates into the ItemsControl.ItemTemplate
Define the data templates in code and reference them using {Static}. For this you can use FuncDataTemplate<>
I've added an issue to track this problem here: https://github.com/AvaloniaUI/Avalonia/issues/1020
You need to use ItemsControl instead of ListBox and have ItemTemplate set differently for each of them.
One will point to DataTemplate(using x:Key, not DataType) with TextBlock, and the other to DataTemplate with Button.
I have to design a calculator like interface.
i'm utterly confused with how to do this.can any provide me with an insight?
how to bind data with these buttons at runtime.?
Ok got this working - basically I created the following XAML layout and with the following bindings:
<Grid x:Name="grdItems">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Button Content="{Binding Items[0].ItemName}"></Button>
<Button Content="{Binding Items[1].ItemName}" Grid.Column="1"></Button>
<Button Content="{Binding Items[2].ItemName}" Grid.Column="2"></Button>
<Button Content="{Binding Items[3].ItemName}" Grid.Row="1"></Button>
<Button Content="{Binding Items[4].ItemName}" Grid.Row="1" Grid.Column="1"></Button>
<Button Content="{Binding Items[5].ItemName}" Grid.Row="1" Grid.Column="2"></Button>
<Button Content="{Binding Items[6].ItemName}" Grid.Row="2"></Button>
<Button Content="{Binding Items[7].ItemName}" Grid.Row="2" Grid.Column="1"></Button>
<Button Content="{Binding Items[8].ItemName}" Grid.Row="2" Grid.Column="2"></Button>
<Button Content="Prev" Command="{Binding MovePrevCommand}" Grid.Row="3"></Button>
<Button Content="{Binding Items[9].ItemName}" Grid.Row="3" Grid.Column="1"></Button>
<Button Content="Next" Command="{Binding MoveNextCommand}" Grid.Row="3" Grid.Column="2"></Button>
</Grid>
This gives you the grid layout
Then I created a collection manager class that would do the 'windowed view'
class TestCollection
{
public ObservableCollection<TestItem> Items { get; set; }
List<TestItem> _items = new List<TestItem>();
int pos = 0;
public TestCollection(int size)
{
MoveNextCommand = new Command(new Action(MoveNext));
MovePrevCommand = new Command(new Action(MovePrev));
Items = new ObservableCollection<TestItem>();
for (int i = 0; i < size; i++)
{
_items.Add(new TestItem("Item " + i.ToString()));
}
UpdateItems();
}
public void MoveNext()
{
pos += 10;
if (pos > _items.Count - 10)
pos = _items.Count - 10;
UpdateItems();
}
public ICommand MoveNextCommand { get; set; }
public ICommand MovePrevCommand { get; set; }
public void MovePrev()
{
pos -= 10;
if (pos < 0)
pos = 0;
UpdateItems();
}
private void UpdateItems()
{
Items.Clear();
foreach (var i in _items.Skip(pos).Take(10))
{
Items.Add(i);
}
}
}
I created a simple implementation of ICommand to call a delegate:
class Command : ICommand
{
Action CallBack = null;
public Command(Action cb)
{
CallBack = cb;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
CallBack();
}
}
TestItem is just a simple class with an ItemName property
class TestItem
{
public string ItemName { get; set; }
public TestItem(string itemName)
{
ItemName = itemName;
}
}
Then in the main app code I added
var i = new TestCollection(2000);
grdItems.DataContext = i;
To wire the collection up to the grid. Works pretty well - you can add command bindings to your items to get the desired effect if you push the buttons (assuming you need buttons for each item of course!)
Let me know if that helps you get started or if there is anything you don't understand (or if this even works on Metro!!)
Edit: Just reading up and it seems IObservableVector now replaces ObservableCollection in WinRT
This was Jan 2012 so it may be that since then updates have added an implementation of ObservableVector - but from this article it seems you need to implement it
http://blogs.u2u.be/diederik/post/2012/01/03/Hello-ObservableVector-goodbye-ObservableCollection.aspx
Code is there anyway so no brain power required!
Maybe it is a simple question, but I can’t find the answer.
I have three User controls that are different only with colour. There is code one of them:
<UserControl x:Class="SilverlightApplication14.NodePicture"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:SilverlightApplication14">
<UserControl.Resources>
<local:NodeViewModel x:Key="Children" />
</UserControl.Resources>
<Grid x:Name="LayoutRootNodePicture" Height="100" Width="100"
HorizontalAlignment="Center" DataContext="{Binding Source={StaticResource Children}, Path=Children}" >
<Canvas x:Name="ParentCanvas" Background="White" Width="100" Height="100" >
<Rectangle Fill="Yellow" Stroke="Blue" Width="100" Height="100" >
</Rectangle >
</Canvas>
<Image HorizontalAlignment="Center"
Source="add.png"
Stretch="Fill"
Width="16"
VerticalAlignment="Top"
Margin="0,0,2,2"
Height="16" MouseLeftButtonDown="Image_MouseLeftButtonDown">
</Image>
</Grid>
</UserControl>
How can I combine them into ObservableCollection Children?
public class NodeViewModel : INotifyPropertyChanged
{
public ObservableCollection<NodeViewModel> Children
{
get { return _children; }
set
{
_children = value;
NotifyChange("Children");
}
}
private void NotifyChange(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
And how can I use then elements of this controls collection?
Is there a simple (or a right way ) way of doing this?
As far as I understood you right, you have 3 user controls which have names something like NodePicture, GreenNodePicture and BlueNodePicture.
First of all, if the 3 controls differ to a very little degree, it would be better to have only one control which switches the color using some property value.
Let's suppose that your controls differ by the background color of the rectangle on the canvas. So I would change your control so:
<Grid x:Name="LayoutRootNodePicture" Height="100" Width="100"
HorizontalAlignment="Center">
<Canvas x:Name="ParentCanvas" Background="{Binding NodeColor}" Width="100" Height="100" >
</Canvas>
<Image HorizontalAlignment="Center"
Source="add.png"
Stretch="Fill"
Width="16"
VerticalAlignment="Top"
Margin="0,0,2,2"
Height="16" MouseLeftButtonDown="Image_MouseLeftButtonDown">
</Image>
</Grid>
I've removed the Resources section because the view shouldn't create new view model objects, it should use an existing DataContext. You can see that the background color of the rectangle is based on the property NodeColor of the view model.
Let's add this property to the view model:
public class NodeViewModel : INotifyPropertyChanged
{
private SolidColorBrush _nodeColor;
public SolidColorBrush NodeColor
{
get { return _nodeColor; }
set
{
_nodeColor = value;
NotifyChange("NodeColor");
}
}
//...
And now if you want to display 3 controls with different color you should create 3 view models with different properties. Here is the example of the red, blue and green viewmodels:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
var redBrush = new SolidColorBrush(Color.FromArgb(255, 255, 0, 0));
var greenBrush = new SolidColorBrush(Color.FromArgb(255, 0, 255, 0));
var blueBrush = new SolidColorBrush(Color.FromArgb(255, 0, 0, 255));
this.DataContext = new MainViewModel
{
Nodes = new ObservableCollection<NodeViewModel>{
new NodeViewModel
{
NodeColor = redBrush,
Children = new ObservableCollection<NodeViewModel>{
new NodeViewModel { NodeColor = greenBrush, LeftOffset = 65, TopOffset = 10},
new NodeViewModel { NodeColor = greenBrush, LeftOffset = 55, TopOffset = 60}
}
}, //red
new NodeViewModel { NodeColor = greenBrush}, //green
new NodeViewModel { NodeColor = blueBrush} //blue
}
};
}
}
public class MainViewModel
{
public ObservableCollection<NodeViewModel> Nodes { get; set; }
}
View models are translated into the views using data templates:
<ListBox ItemsSource="{Binding Nodes}">
<ListBox.ItemTemplate>
<DataTemplate>
<local:NodePicture DataContext="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I haven't used the Children property because I haven't understood where to use it. Maybe child nodes are displayed on the canvas. Anyway if it is important - you can provide additional information and I'll help with this.
Update:
The easiest way to draw child items on the canvas is to add the dependency property which updates the canvas when the collection is updated:
public partial class NodePicture : UserControl
{
public NodePicture()
{
InitializeComponent();
}
public IEnumerable<NodeViewModel> ChildViewModels
{
get { return (IEnumerable<NodeViewModel>)GetValue(ChildViewModelsProperty); }
set { SetValue(ChildViewModelsProperty, value); }
}
public static readonly DependencyProperty ChildViewModelsProperty =
DependencyProperty.Register("ChildViewModels", typeof(IEnumerable<NodeViewModel>), typeof(NodePicture),
new PropertyMetadata(null, (s, e) => ((NodePicture)s).UpdateCanvas()));
private void UpdateCanvas()
{
this.ParentCanvas.Children.Clear();
var items = this.ChildViewModels;
if(items == null)
return;
var controls = items.Select(item=>
{
var e = new Ellipse{Width = 20, Height = 20};
e.Fill = item.NodeColor;
//Or using the data binding
//BindingOperations.SetBinding(e, Ellipse.FillProperty, new Binding("NodeColor") { Source = item });
Canvas.SetLeft(e, item.LeftOffset);
Canvas.SetTop(e, item.TopOffset);
return e;
});
foreach(var c in controls)
this.ParentCanvas.Children.Add(c);
}
Where the TopOffset and LeftOffset are the properties of the NodeViewModel class.
After that you should set this property in the xaml code:
<DataTemplate>
<local:NodePicture DataContext="{Binding}" ChildViewModels="{Binding Children}" />
</DataTemplate>
It won't work with the ObservableColelction class because i didn't handle the CollectionChanged event.
Another approach - to use the ListBox control with the custom ItemsPanelTemplate and ListBoxItem ControlTemplate. But it is the much more complex solution.