I have been developing my first MVVM WPF application, in which I want to draw a graph containing nodes and edges. Currently I am doing all drawing logic in the code behind of my view, iterating over the nodes, creating shapes accordingly and adding them to a canvas.
Because I do not want to keep track of the shapes, and just want them to be drawn based on the data that is given (i.e. the nodes) I have decided to create an ObservableCollection of both the Nodes and Edges, and bind an ItemsControl to these in order to automatically draw the shapes.
For now I am focussing on drawing the nodes, and came up with the following XAML code:
<ItemsControl x:Name="ItemsControlCanvas" ItemsSource="{Binding Path=Nodes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="{Binding ?}" ScaleY="{Binding ?}"/>
</Canvas.LayoutTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="Blue" Width="4" Height="4" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Path=Position.Y}" />
<Setter Property="Canvas.Left" Value="{Binding Path=Position.X}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
In my view, I have two properties ScaleX and ScaleY which I set when the user scrolls on the canvas. In my earlier code behind version, I would create a ScaleTransform with those properties, and apply it to the named canvas using a LayoutTransform.
public partial class GraphOverview : Page
{
public double ScaleX { get; set; }
public double ScaleY { get; set; }
The problem in this version is that I cannot get around to get it working in my new XAML only code. I would like for the ScaleX and ScaleY attributes of the ScaleTransform in the ItemsPanelTemplate to be bound to the properties in my view. Only, normal ElementName binding does not work for some reason. More clearly, the canvas is presumably not aware of the view, I assume because it is a template. Moreover, I cannot call the canvas from the code behind, even when it has a name.
I have tried several solutions, fumbling with RelativeResources and the like, but I think I do not clearly understand why a ItemsPanelTemplate is so disconnected from everything else in the XAML code. Thanks in advance!
Update
Maybe I should clarify that this could be easily resolved by moving the ScaleX and ScaleY properties to the ViewModel, and bind to those properties. But in my opinion such View-specific properties should not reside in the ViewModel.
You are correct when you say view specific information should not be in the view model. But in your case, the positions becomes part of a model because the positions are nothing but data based on which your view should behave. In such cases you can consider them as part of model rather than view. Write a separate Model Class and use it in the observable collection of your ViewModel Class. I hope it clears your doubt.
public class Node
{
public double ScaleX { get; set; }
public double ScaleY { get; set; }
}
Edit
Answering your questions. -
But the Scale properties specify how the Canvas should behave, so does the View not already serve as a 'model' for this? It seems superfluous to me to create a new Model just to set the scale properties of a single Canvas? The view should be merely concerned with drawing data, and since the properties serve as modifications of these drawings, they belong in the View, don't they?
You want to draw the graph based on the data provided. The Node class is going to provide the data for you. It stores the data and not the logic how the canvas should render, the XAML code uses this data and has the logic to show it in the view.
so does the View not already serve as a 'model' for this?
The implementation you have tried has the model in the view, the idea is to separate the model from the view. It might look superfluous initially to create a class just to hold 2 double values. But there are some advantages if you abstract them away from the view.
In the view model you can create a observable collection for this Node class(Model). You cannot access it from Viewmodel if you have the ScaleX an ScaleY in View.
In future you might want to change ScaleX and ScaleY to be dervied into different scale e.g. Logarmic scale/ Different unit. In such cases you will have to change the logic in ViewModel to do so and never have to worry about changing the View. But if you have this Observable collection in the view, you have to change the view for making a change to the data/model.
Lastly - you can write unit tests for whatever you have in the ViewModel but not the View.
Normally ScaleX and ScaleY will be part of the view, but in your case they change and stores data. Hence you need to abstract this ScaleX and ScaleY into a different layer for preserving the MVVM concept.
I'm not sure if you have set the DataContext of your view, it would help to show more of your code behind
internal ViewModel viewModel { get; set; }
public View()
{
DataContext = (viewModel = new ViewModel());
}
Are you binding the ObservableCollection to ItemSource of the nodes? I'm not familiar with drawing, but when I've bound to children, I've never achieved results.
But I've only developed about 4 projects utilizing MVVM.
Related
I’ve got an ObservableCollection of objects that are classified with a name and a set of 3D coordinates. I’ve also got an ObservableCollection of layers, each supposed to hold a 2D grid.
Problem Setting
The objective is to use an ItemsControl, probably nested, to display those objects in the following fashion: if the Z coordinate determines the layer, and the X and Y coordinates specify each object’s position on the grid, then the object should be displayed in a TabControl, where the TabItem corresponds to the Z coordinate and hosts a Grid where the Grid.Row and Grid.Column attributes determine where the object’s name is written on the TabItem.
Important: while each 3D coordinate is only used once, one “Entry” object may have multiple 3D coordinates, and as such may appear various times on a grid and/or different TabItems.
Note: the data model is not carved in stone; if the problem can be solved with another data model, I’m open to change it. The display, however, is a customer requirement—it might be modified, but I’d need extremely good arguments for that.
Background Info
The objects look like this (BindableBase is from the Prism Library):
public class Entry : BindableBase {
public string Name { set; get; }
private EntryCoordinates coordinates;
public EntryCoordinates Coordinates {
set { SetProperty(ref coordinates, value); }
get { return coordinates; }
}
}
public class EntryCoordinates : BindableBase {
private int x;
public int X {
set { SetProperty(ref x, value); }
get { return x; }
}
private int y;
public int Y {
set { SetProperty(ref y, value); }
get { return y; }
}
private int z;
public int Z {
set { SetProperty(ref z, value); }
get { return z; }
}
}
The Entry objects are hosted in “entry layers”:
public class EntryLayer : ObservableCollection<Entry> {
}
Eventually, I want to be able to modify the Entry objects (which are more complex in reality) via the UI, so two-way data binding is an absolute necessity.
Effort so far
Using #Rachel’s excellent WPF Grid Extension, I implemented an ItemsControl that populates a Grid as desired:
<ItemsControl ItemsSource="{Binding EntryLayers, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid GridExtension.RowCount="{Binding RowCount}"
GridExtension.ColumnCount="{Binding ColumnCount}"
GridExtension.StarRows="{Binding StarRows}"
GridExtension.StarColumns="{Binding StarColumns}"
IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Row" Value="{Binding Coordinates.X}"/>
<Setter Property="Grid.Column" Value="{Binding Coordinates.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<GridCellThumb XCoordinate="{Binding Coordinates.X}"
YCoordinate="{Binding Coordinates.Y}">
<Thumb.Template>
<ControlTemplate>
<TileControl Entry="{Binding}"/>
</ControlTemplate>
</Thumb.Template>
</GridCellThumb>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The GridCellThumb is a custom control that allows for drag and drop (omitted here for clarity’s sake) and exposes coordinate dependency properties:
public class GridCellThumb : Thumb {
public static readonly DependencyProperty XCoordinateProperty = DependencyProperty.Register("XCoordinate", typeof(int), typeof(GridCellThumb));
public int XCoordinate {
get { return (int)GetValue(XCoordinateProperty); }
set { SetValue(XCoordinateProperty, value); }
}
public static readonly DependencyProperty YCoordinateProperty = DependencyProperty.Register("YCoordinate", typeof(int), typeof(GridCellThumb));
public int YCoordinate {
get { return (int)GetValue(YCoordinateProperty); }
set { SetValue(YCoordinateProperty, value); }
}
}
The TileControl is a user control that displays an Entry’s name:
<StackPanel Orientation="Vertical">
<Label Content="{Binding Path=Name}" />
</StackPanel>
I’m stuck in finding out how to wrap the original ItemsControl in a TabControl template so that the objective of displaying an entry, possible various times, correctly. For instance, binding to the Coordinates.Z path works, but creates as many TabItems as there are entries:
<TabControl ItemsSource="{Binding Entries}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding Coordinates.Z}" />
<Setter Property="TabIndex"
Value="{Binding Coordinates.Z}" />
</Style>
</TabControl.ItemContainerStyle>
<!-- the ItemsControl goes here -->
</TabControl>
I’ve tried the solutions proposed by #greg40 (nested ItemsControl), #d.moncada (another nested ItemsControl), and #Sheridan (going up the visual tree), but I always gloriously fail when relating an Entry to a given EntryLayer.
Does anyone have further ideas to explore? As I said, I’m also open to re-structuring my data model, if that leads to an easier solution.
Update 2018-01-08
I’ve explored the path of using Buttons instead of a TabControl in the sense of data-binding their clicked state to displaying varying information on the grid. This, however, only shifted the problem and created a new one: the data isn’t pre-loaded anymore, which is crucial to the customer’s requirements.
I’m now strongly considering to suggest a change of requirements to the customer and devise a different data model altogether. In order to avoid taking a wrong route again, I’d very much appreciate if there was someone in the community with a strong opinion about this problem that they’d be willing to share with me.
After some very valuable offline advice from a peer expert, I decided against the customer suggested data model and provide them with an alternative solution. The approach is now to fill the View in a top-down fashion.
In essence, this means that what was previously my Z coordinate is now the index of a dedicated EntryLayer object defining the TabItems. Each EntryLayer has an ObservableCollection<Entry> that refers to those Entrys that a parts of that EntryLayer.
This contrasts to the initial model in the sense that now it’s not possible anymore that a Entry can have multiple coordinates; instead, the Entry itself might exist multiple times with different coordinates.
How did I sell it to the customer? I told them that now an Entry could have customization options that may differ between its representations on the same or other EntryLayers, e.g., by using user-specified colors and fonts, while still pointing back to the same base information. It gives me some more work to do implementation-wise, but it elegantly solves the deadlock by putting it in the shape of a new feature.
Let me state problem first. I would like to implement wrapper around Canvas (let me call it Page) which would implement selecting rectangle around its UIElements which are actually selected.
For this I implemented ISelect interface like so :
interface ISelect {
Point Center {get; set;} //Center of selecting rectangle
Size Dimensions {get; set;} //Dimensions of selecting rectangle
}
Every object that is put to Page implements ISelect interface.
Page has SelectedElements of type ObservableCollection which holds reference to all currently selected elements.
For every entry in SelectedElements i would like to draw rectangle around it.
I have few ideas how to do this :
Every UIElement can implement on its own this rectangle and show it when selected. This option would require for new objects to implement this every time. So I rather not use it.
In Page I could create rectangles in code-behind in add them to the Page. It isn't MVVM recommended priniciple.
In Page XAML create somehind like ItemsControl and bind it to SelectedElements with specific template. This option seems like the best one to me. Please help me in this direction. Should I somehow use ItemsControl?
Thank you.
I don't have time to dig a complete working solution, so this is mostly a collection of suggestions.
Each element should have view model
public abstract class Element: INotifyPropertyChanged
{
bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
public class EllipseElement : Element {}
public class RectangleElement : Element {}
Then there are data templates to visualize elements (I can't give you converter code, but you can replace it with another, look here).
<DataTemplate DataType="{x:Type local:EllipseElement}">
<Border Visibility="{Binding IsSelected, Converter={local:FalseToHiddenConverter}}">
<Ellipse ... />
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type local:RectangleElement}">
<Border Visibility="{Binding IsSelected, Converter={local:FalseToHiddenConverter}}">
<Rectangle ... />
</Border>
</DataTemplate>
Then bind ObservableCollection of elements to canvas (which is tricky, see this answer, where ItemsControl is used to support binding).
Your selection routine has to hit-test elements and set/reset their IsSelected property, which will show border. See here regarding how to draw over-all selection rectangle.
Basically, I'm not sure how to use MVVM, and/or use commands correctly in my current situation. So, I have a View, containing a list box, and a panel of animation objects, that I created. These animation objects can be animated through a simple public method, Animate(). The goal here, is to associate this Animate() method with buttons inside the list box, like so:
As we can see in the diagram below, both the ListBox items and the visual elements inside of the animation area are associated with the same collection of models from the ViewModel, with the items in each being templated. For example, the ListBox items are simply defined to have some text related to a data item, and the AnimationObjects take on an appearance according to the data. These models, I feel, should not understand that an animation is occurring - they're simple data, and the animation does not change them.
Finally, I show in the below diagram, that I have created two FrameworkElement child types, one for holding animation objects, and another that defines these animation objects.
How can I connect this animation action to the buttons within the list box? It doesn't make sense to me that the models/viewmodels know about the animation, because it doesn't change the state of anything in my application - it's just for visual purposes. I've thought about using a RoutedCommand defined in AnimationObject, and having the buttons bind their command property accordingly, but I worry that will simply make every element animate at the same time.
It is also important for my sake, that I conform to MVVM, as these data will be used in many other situations, perhaps even a different version of this view.
Any advice would be appreciated.
What you can do is call a command in your ViewModel , i.e. the DataContext of your ListBox.
CS :
public class ViewModel
{
public ICommand AnimateObjectCommand { get; }
}
XAML :
<DataTemplate x:Key="AnimationObjectItemTemplate">
<Button Command="{Binding Path=DataContext.AnimateObjectCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" />
</DataTemplate>
<ListBox ItemsSource="{Binding AnimationObjects}" ItemTemplate="{StaticResource AnimationObjectItemTemplate}"/>
your Command implementation should be one that accepts an argument which would be passed by the CommandParameter .
private ICommand _animateObjectCommand;
public ICommand AnimateObjectCommand
{
get
{
if (_animateObjectCommand == null)
{
_animateObjectCommand = new RelayCommand<AnimationObject>( ao => { ao.Animate(); });
}
return _animateObjectCommand;
}
}
The CommandParameter = {Binding} meaning this.DataContext where this is an Item in your ListBox and it's DataContext is an AnimationObject.
i've written a tool that generates sql queries using GUI, i want to rewrite the tool using MVVM and WPF, every sql column type has a different control as you can see in the following image
i add a column filter control based on the sql column type, and i generate the controls using code, just like i used to do in windows forms.
in MVVM i've read that the view is writtien enteirly using XAML,
does MVVM suite such application where i have to add different user
controls dynamically to a stack panel?
The controls won't exist in the view unless some column is double clicked, that means the control won't be available in the xaml and won't be hidden or collapsed.
is there any way that i can avoid the bindings in the code behind?
should i create a user control for each column type?
in general what is the best approach to devlop such application with complex and dynamic ui using mvvm?
Guess I know how to achieve that, but it is very complex stuff. First you should comprehend MVVM basic concepts.
Main ViewModel should be a class with ObservableCollection of ViewModels, each of them represents a column with its data and properties.
interface IViewModel : INotifyPropertyChanged,IDisposable
{
}
interface IColumnViewModel : IViewModel
{
}
class ViewModelBase : IViewModel
{
// ... MVVM basics, PropertyChanged etc. ...
}
class MainViewModel : ViewModelBase
{
ObservableCollection<IColumnViewModel> Columns {get; set}
}
In View I suppose something like ItemsControl with ItemTemplate, that should embed ContentControl with DataTemplate, that shall be automatically selected by WPF according to binded DataContext of list item. StackPanel itself is not suitable for that, but it can be invoked as ItemsPanelTemplate
<Window
xmlns:v="clr-namespace:WpfApplication.Views"
xmlns:vm="clr-namespace:WpfApplication.ViewModels">
<Window.Resources>
<DataTemplate DataType="{x:Type TypeName=vm:TextColumnViewModel}">
<v:TextColumnView/>
</DataTemplate>
</Window.Resources>
<ItemsControl
ItemsSource="{Binding Columns}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
So, you should build View/ViewModel pair for every column type.
Hope, my example will help. Good luck with your girlfriend and MVVM :)
If I've understood your scenario correctly :
You can use Data Templates & Items Templates
For example I've written an application which loads Data into Collection and then shows each item of that collection in a Wrap Panel [ Or stack panel ] based on defined data template.
And Wrap penel items are in sync by the collection itself within two way binding
You should consider using Observable Collections to achieve this goal
Then you can fill the collection and see the results on a view
I hope this helps
To write something like this in MVVM, you would have one view that is say, your content area. That view would have a view model, one of the properties of that view model would be a view, or several properties of that view model would be a view. It takes a bit to wrap your head around at times, but if you use Inversion of Control and Dependency Injection properly a view of views is very manageable in an MVVM pattern.
Well, your view isn't written entirely in XAML - you generate controls in C#.
I don't think you'll gain something from rewriting this and fitting it into an MVVM mold. Just keep the code as it is now and enjoy.
I have a WPF window displaying different self-defined Views. So far I was able to use everything I learned about MVVM :)
Now I got to a new "problem": I have 10 entities of the same view in a bigger view. These ten view-entities contain a set of controls (textbox, combobox etc.) but are all consistent.
So how do I bind these Views to a ViewModel?
I thought about having 10 instances of the ViewModel in the "higher-level" ViewModel and give the views fix-defined the instances of the VM as datacontext.
My question is now --> Is there a easier (or more convienient) way to bind many (identical) views to their viewmodels?
Code-Example:
View Model:
private PanelViewModel _panelViewModel1 = new PanelViewModel();
public PanelViewModel PanelVM1
{
get { return _panelViewModel1; }
}
View-Example:
<myControls:vwPanel HorizontalAlignment="Left" x:Name="vwPanel1"
VerticalAlignment="Top" DataContext="{Binding Path=PanelVM1}"/>
What bothers me is that I would need this logic ten times for ten views?
UPDATE:
To answer some questions: I want to show one view 10 times (in my example) I defined my own view by inheriting from UserControl. So my vwPanel inherits from UserControl. The 10 vwPanels are just placed inside a StackPanel inside a Grid.
It's not about displaying data, as you pointed out, there would be a listview or a datagrid a better place to start. It's a special case where I need this much input-controls :/
UPDATE2: What I hoped for was more like defining a List of ViewModels and Bind my 10 Views to one of this List. But this will not work will it? At least I wouldn't know how to refernce one "special" entitiy in the list out of XAML...
Typically I use implicit DataTemplates for mapping Views to ViewModels. They can go in <Application.Resources>, <Window.Resources> or even in under specific elements only such as <TabControl.Resources>
<DataTemplate DataType="{x:Type local:PanelViewModel}">
<myControls:vwPanel />
</DataTemplate>
This means that anytime WPF encounters an object in the VisualTree of type PanelViewModel, it will draw it using vwPanel
Objects typically get placed in the VisualTree through an ItemsSource property
<ItemsControl ItemsSource="{Binding CollectionOfAllPanels}" />
or by using a ContentControl
<ContentControl Content="{Binding PanelVM1}" />
If I understand your question correctly, you have a collection of something that you what to represent visually. That is, you have several viewmodels that you want to define a single view for, but show X number of times. Your example shows you using a panel as your view for the "PanelViewModel"...what is the parent item's control for the vwPanel? Assuming you're using something like a ListBox, you can define a custom DataTemplate that contains your vwPanel and assign that DataTemplate to your ListBox.ItemTemplate.
For example:
<Window.Resources>
<DataTemplate x:Key="myVMTemplate" TargetType="{x:Type myViewModels:PanelViewModel}">
<myControls:vwPanel HorizontalAlignment="Left" VerticalAlignment="Top"/>
</DataTemplate>
</Window.Resources>
<ListBox ItemsSource="{Binding Path=MyCollectionOfPanelVMs}"
ItemTemplate="{StaticResource myVMTemplate}" />
I haven't verified that this works.