I would like to draw in canvas different shapes. How can I make object from ArrowsItems ObservableCollection and CircleItems ObservableCollecton visible in canvas? I am also creating Shapes ObservableCollection including every Circle and Arrows Items. I think that propably the reason is in the data binding but don't know where.
The goal is possibility to generate and then draw programmatically circles and arrows.
<Button Grid.Row="1" MaxWidth="1000" Command="{Binding CreateEllipse}">Utwórz</Button>
<Viewbox Grid.Row="2" Margin="0 20 0 0" Stretch="Uniform" StretchDirection="Both" VerticalAlignment="Stretch">
<ItemsControl Name="Shape" ItemsSource="{Binding Shapes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="2000" Height="1200" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X, Mode=TwoWay}"/><Setter Property="Canvas.Top" Value="{Binding Y, Mode=TwoWay}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type core:CircleItem}">
<Viewbox Width="{Binding Width}" Height="{Binding Height}">
<!--MouseMove="Viewbox_MouseMove"-->
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseMove">
<i:InvokeCommandAction Command="{Binding DataContext.MyCommand, ElementName=Shape}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<i:Interaction.Behaviors>
<local:DragBehavior/>
</i:Interaction.Behaviors>
<Grid>
<Grid.RenderTransform>
<TranslateTransform X="{Binding TransformX}" Y="{Binding TransformY}" />
</Grid.RenderTransform>
<Ellipse Width="{Binding Width}" Height="{Binding Height}" Fill="{Binding Color}" />
<TextBlock HorizontalAlignment="Center" Text="{Binding Text}" TextAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate DataType="{x:Type core:ArrowItem}">
<Line X1="{Binding X1}" Y1="{Binding Y1}" X2="{Binding X2}" Y2="{Binding Y2}" Stroke="{Binding Color}" StrokeThickness="{Binding StrokeThickness}" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Viewbox>
Also in my ViewModel:
public ObservableCollection<CircleItem> CircleItems { get; set; }
public ObservableCollection<ArrowItem> ArrowItems { get; set; }
public CompositeCollection Shapes { get; set; }
And after adding some objects of CircleItem class to CircleItems and ArrowItem to ArrowItems:
CompositeCollection coll = new CompositeCollection();
coll.Add(new CollectionContainer() { Collection = CircleItems });
coll.Add(new CollectionContainer() { Collection = ArrowItems });
Shapes = coll;
Make sure you initialize the Shapes property before the view model is assigned to the DataContext. The collection properties should all be readonly, otherwise you would have to fire a property change notification from their setters.
public class ViewModel
{
public ObservableCollection<CircleItem> CircleItems { get; }
= new ObservableCollection<CircleItem>();
public ObservableCollection<ArrowItem> ArrowItems { get; }
= new ObservableCollection<ArrowItem>();
public CompositeCollection Shapes { get; }
= new CompositeCollection();
public ViewModel()
{
Shapes.Add(new CollectionContainer { Collection = CircleItems });
Shapes.Add(new CollectionContainer { Collection = ArrowItems });
}
}
If I understood you correctly, you want to have different templates for different datatypes. This is quite easy to get. One of the ways (probably the simplest one):
create data templates for every of your types you want to show
create some kind of data template that will select the proper template:
<!-- Data template for arrows -->
<DataTemplate x:Key="ArrowsDataTemplate" DataType="{x:Type core:ArrowItem}">
<!-- Create here your template for arrows -->
</DataTemplate>
<!-- create data templates for other stuff and then "selector" data template -->
<DataTemplate x:Key="ContentDataTemplate">
<ContentPresenter x:Name="itemContentPresenter"
ContentTemplate="{StaticResource CircleDataTemplate}" <!-- just the default one -->
Content="{TemplateBinding Content}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding MyItemTypeAsEnum}" Value="Arrow">
<Setter TargetName="itemContentPresenter" Property="ContentTemplate" Value="{StaticResource ArrowsDataTemplate"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<!-- Now in your control (for example content control), where you show this stuff, do: -->
<ContentControl ContentTemplate="{StaticResource ContentDataTemplate}"/>
Now, I assumed that you have one base Item with property MyItemTypeAsEnum which will give you Circle for CircleItem, Arrow for ArrowItem etc. But if you don't have such property, you should be able to get boolean value from your viewmodel that will tell you if this item is Circle or not etc.
Into your main control, when you show thigs you have to set ContentTemplate to your "selector" template.
Related
based on ItemsControl only shows first Item of each object I need your help again.
The solution worked out very well but after vacation and doing other stuff I need to modify it.
Actually there are Objects (CanvasObject) which are drawn on startup/after reading xml data and others (Module) which are dropped into the canvas from a treeview. These Modules are showing up in the canvas visual children but I cant see them at the canvas.
This is my code so far - I tried different approaches but always got similar results:
C#:
public class CanvasLine
{
public Point P1 { get; set; }
public Point P2 { get; set; }
public Brush Stroke { get; set; }
public double StrokeThickness { get; set; }
public DoubleCollection StrokeDashArray { get; set; }
}
public class CanvasObject
{
public String Name { get; set; }
public ObservableCollection<CanvasLine> CanvasLines { get; set; }
}
public class Module
{
public Dictionary<String, String> Attributes { get; set; }
public Point AnchorPoint { get; set; }
public Double Height { get; set; }
public Double Width { get; set; }
}
public class ViewModel
{
...
public ObservableCollection<Object> CanvasObjects;
...
}
The Viewmodels ObservableCollection<Object> gets filled with CanvasObjects from a xml - these ones are shown correctly. After that I'm adding Modules to that Collection by dropping them to the canvas. These ones seems to be into the canvas children collection but are not visible.
Here is the corresponding / cleaned up XAML:
<ItemsControl Grid.Column="1" Grid.Row="2" Margin="0" ItemsSource="{Binding CanvasObjects, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.Resources>
<ScaleTransform x:Key="LineTransform"
ScaleX="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
ScaleY="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"/>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" AllowDrop="True" Drop="Canvas_OnDrop"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplateSelector>
<xmlTemp:TemplateSelector>
<xmlTemp:TemplateSelector.ModuleTemplate>
<DataTemplate DataType="{x:Type xmlElements:Module}">
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource Self}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding AnchorPoint.X}"/>
<Setter Property="Canvas.Top" Value="{Binding AnchorPoint.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Width="{Binding Width}" Height="{Binding Height}" StrokeThickness="2" Fill="Black" Stroke="Black">
<Rectangle.RenderTransform>
<StaticResource ResourceKey="LineTransform"/>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</xmlTemp:TemplateSelector.ModuleTemplate>
<xmlTemp:TemplateSelector.LineObjectTemplate>
<DataTemplate DataType="{x:Type dt:CanvasObject}">
<ItemsControl ItemsSource="{Binding CanvasLines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path Stroke="{Binding Stroke}"
StrokeDashArray="{Binding StrokeDashArray}">
<Path.Data>
<LineGeometry
Transform="{StaticResource LineTransform}"
StartPoint="{Binding P1}"
EndPoint="{Binding P2}"/>
</Path.Data>
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="StrokeThickness">
<Setter.Value>
<MultiBinding Converter="{StaticResource MultiplicationConverter}">
<Binding Path="StrokeThickness" Mode="OneWay"/>
<Binding Source="3"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="StrokeThickness" Value="{Binding StrokeThickness, Mode=OneWay}"/>
</Trigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</xmlTemp:TemplateSelector.LineObjectTemplate>
</xmlTemp:TemplateSelector>
</ItemsControl.ItemTemplateSelector>
</ItemsControl>
Here are the missing event and template selector:
private void Canvas_OnDrop(Object sender, DragEventArgs e)
{
Module Module = e.Data.GetData("Module") as Module;
Canvas Canvas = sender as Canvas;
if (Module != null && Canvas != null)
{
Module.Width = 0.1;
Module.Height = 0.2;
Module.AnchorPoint = e.GetPosition(Canvas);
ViewModel.CanvasObjects.Add(Module);
}
}
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate LineObjectTemplate { get; set; }
public DataTemplate ModuleTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is CanvasObject)
return LineObjectTemplate;
if (item is Module)
return ModuleTemplate;
return base.SelectTemplate(item, container);
}
}
If I'm changing the ModuleTemplates ItemsControl to a ListBox I can see at least that there is some selected element in the top left corner after dropping a Module somewhere in the canvas. I'm thinking that the Rectangles Container might have no size but I'm not sure how to handle that correctly. Also the dropping position has no effect.
Possibly it's just a stupid error, just like the transparent, not dropable/hitable background which costs me over an hour today and I'm "seeing" it tomorrow after a few hours of sleep but every help and hint is highly appreciated!
Greetings
Edit:
THX to elgonzo I found the error. The Modules ItemsControl was unnecessary! THX again, perfect time to finish work now ;)
Following Code works fine:
<ItemsControl Grid.Column="1" Grid.Row="2" Margin="0" ItemsSource="{Binding CanvasObjects, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.Resources>
<ScaleTransform x:Key="LineTransform"
ScaleX="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
ScaleY="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"/>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Transparent" AllowDrop="True" Drop="Canvas_OnDrop"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplateSelector>
<xmlTemp:TemplateSelector>
<xmlTemp:TemplateSelector.ModuleTemplate>
<DataTemplate DataType="{x:Type xmlElements:Module}">
<DataTemplate.Resources>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding AnchorPoint.X}"/>
<Setter Property="Canvas.Top" Value="{Binding AnchorPoint.Y}"/>
</Style>
</DataTemplate.Resources>
<Rectangle Width="{Binding Width}" Height="{Binding Height}" StrokeThickness="2" Fill="Black" Stroke="Black">
<Rectangle.RenderTransform>
<StaticResource ResourceKey="LineTransform"/>
</Rectangle.RenderTransform>
</Rectangle>
</DataTemplate>
</xmlTemp:TemplateSelector.ModuleTemplate>
<xmlTemp:TemplateSelector.LineObjectTemplate>
<DataTemplate DataType="{x:Type dt:CanvasObject}">
<ItemsControl ItemsSource="{Binding CanvasLines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Path Stroke="{Binding Stroke}"
StrokeDashArray="{Binding StrokeDashArray}">
<Path.Data>
<LineGeometry
Transform="{StaticResource LineTransform}"
StartPoint="{Binding P1}"
EndPoint="{Binding P2}"/>
</Path.Data>
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="StrokeThickness">
<Setter.Value>
<MultiBinding Converter="{StaticResource MultiplicationConverter}">
<Binding Path="StrokeThickness" Mode="OneWay"/>
<Binding Source="3"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="StrokeThickness" Value="{Binding StrokeThickness, Mode=OneWay}"/>
</Trigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</xmlTemp:TemplateSelector.LineObjectTemplate>
</xmlTemp:TemplateSelector>
</ItemsControl.ItemTemplateSelector>
</ItemsControl>
Now I have the following code:
ViewModel(s)
public class VMBase
{
public string TabID{get;set;}
public string TabHeader {get;set;}
}
public class VM1:VMBase //implements the properties in base class
{
}
public class VM2:VMBase //implements the properties in base class
{
}
And in my DataTemplate.xaml, I have the different local controls binded to the ViewModel, depending on the type of ViewModel it is, ie:
<DataTemplate DataType="{x:Type VM:VM1}">
<local: Control1 />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:VM2}">
<local: Control2 />
</DataTemplate>
Control1 and Control2 are different types of UserControl:
public class Control1:UserControl
{
}
public class Control2:UserControl
{
}
Things are still manageable when I have only two derived classes for VMBase, but what if I have ten? Or more? It's going to get ugly.
Is it possible to bind a single VM to different views ( user control), so that I don't have to manually create so many derived class for VMBase? I will just need to specify the VM properties such as TabID and TabHeader, correct views will be bind as a result.
Edit:
Here are further details: my VM is bind to a ContentControl (ie: contentcontrol.Content=VM). And each VM has two properties TabID and Header. Whether the DataTemplateSelector should be invoked depends on whether it has a specific TabID ( if it has other TabID then this DataTemplateSelector shouldn't be invoked), and which DataTemplate ( the logic inside the DataTemplateSelector ) to invoke depends further on the Header. How to implement this?
Updated answer - v2 (as per question edit)
I think simply returning a null in your DataTemplateSelector when TabID is not a match should do the trick, as WPF will then try and pick the next best match (i.e. the template that matches the DataType). In case the TabID is a match, you can return the template based on the TabHeader value.
So your custom DataTemplateSelector would look like this:
public class TabHeaderDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var element = container as FrameworkElement;
if (element == null)
return null;
var viewModel = item as VMBase;
if (viewModel == null || viewModel.TabID != "02")
return null; //continue only if TabID is a match
if (viewModel != null)
{
switch(viewModel.TabHeader)
{
case "two":
return element.FindResource($"Template2") as DataTemplate;
case "three":
return element.FindResource($"Template3") as DataTemplate;
}
}
return null;
}
}
Sample XAML
<Window.Resources>
<!-- data template for VM1 -->
<DataTemplate DataType="{x:Type local:VM1}">
<Grid>
<Rectangle Stroke="Black" />
<TextBlock Margin="5" Text="{Binding TabHeader}" FontSize="18"/>
</Grid>
</DataTemplate>
<!-- data template for VM2 -->
<DataTemplate DataType="{x:Type local:VM2}">
<Grid>
<Rectangle Stroke="Red" />
<TextBlock Margin="5" Text="{Binding TabHeader}" FontSize="18"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="Template2">
<Grid>
<Ellipse Stroke="Green" StrokeThickness="4"/>
<TextBlock Margin="10" Text="{Binding TabHeader}" FontSize="24"
Foreground="Red" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="Template3">
<Grid>
<TextBlock Margin="10" Text="{Binding TabHeader}" FontSize="24"
Foreground="White" Background="Black" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
<Style TargetType="ContentControl">
<Setter Property="ContentTemplateSelector">
<Setter.Value>
<local:TabHeaderDataTemplateSelector />
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Margin="25">
<ContentControl Content="{Binding VmObj_1}" />
<ContentControl Content="{Binding VmObj_2}" />
<ContentControl Content="{Binding VmObj_3}" />
<ContentControl Content="{Binding VmObj_4}" />
</StackPanel>
and the code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new
{
VmObj_1 = new VM1 { TabID = "01", TabHeader = "one" },
VmObj_2 = new VM1 { TabID = "02", TabHeader = "two" },
VmObj_3 = new VM2 { TabID = "02", TabHeader = "three" },
VmObj_4 = new VM2 { TabID = "03", TabHeader = "four" },
};
}
}
public class VMBase
{
public string TabID { get; set; }
public string TabHeader { get; set; }
}
public class VM1 : VMBase { }
public class VM2 : VMBase { }
Updated answer - v1
You can approach this problem in two different ways. Each option has its own set of pros and cons; but my most recommended approach would be (as #jon-stødle suggested) is to use a DataTemplateSelector
Option 1 - Use DataTemplateSelector
As you are using Type based data-template(s) - then I assume you are most probably using a ContentControl (or a variant) to display the dynamic view-model driven UI. ContentControl and other templated controls such as Label, UserControl, ItemsControl, ListBox etc. usually have dependency property like ContentTemplateSelector or ItemTemplateSelector that you can bind your template-selector to.
You can refer this link for an example for using Label with DataTemplateSelector; or following example for usage with TabControl
XAML:
<Window.Resources>
<DataTemplate x:Key="Tab1Template">
<Grid>
<Rectangle Stroke="Black" />
<TextBlock Margin="5" Text="{Binding}" FontSize="18"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="Tab2Template">
<Grid>
<Ellipse Stroke="Green" StrokeThickness="4"/>
<TextBlock Margin="10" Text="{Binding}" FontSize="24"
Foreground="Red" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="Tab3Template">
<Grid>
<TextBlock Margin="10" Text="{Binding}" FontSize="24"
Foreground="White" Background="Black" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
<Style TargetType="{x:Type TabControl}">
<Setter Property="ContentTemplateSelector">
<Setter.Value>
<local:TabIdDataTemplateSelector />
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<TabControl>
<TabControl.ItemsSource>
<col:ArrayList>
<sys:String>1</sys:String>
<sys:String>2</sys:String>
<sys:String>3</sys:String>
</col:ArrayList>
</TabControl.ItemsSource>
</TabControl>
DataTemplateSelector:
public class TabIdDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var element = container as FrameworkElement;
if (element == null)
return null;
//var vm = item as VMBase;
//var id = vm.TabId;
string id = item as string;
if (id != null)
{
return element.FindResource($"Tab{id}Template") as DataTemplate;
}
return null;
}
}
Option 2 - Use Style based data-triggers
Another option is use data-trigger(s) on the property (i.e. TabId) of your ViewModel to update the ContentTemplate of your container-view (i.e. TabControl).
<Window.Resources>
<DataTemplate x:Key="Tab1Template">
<Grid>
<Rectangle Stroke="Black" />
<TextBlock Margin="5" Text="{Binding Key}" FontSize="18"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="Tab2Template">
<Grid>
<Ellipse Stroke="Green" StrokeThickness="4"/>
<TextBlock Margin="10" Text="{Binding Key}" FontSize="24"
Foreground="Red" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="Tab3Template">
<Grid>
<TextBlock Margin="10" Text="{Binding Key}" FontSize="24"
Foreground="White" Background="Black" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
<Style TargetType="{x:Type TabControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding TabId}" Value="1">
<Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=Tab1Template}" />
</DataTrigger>
<DataTrigger Binding="{Binding TabId}" Value="2">
<Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=Tab2Template}" />
</DataTrigger>
<DataTrigger Binding="{Binding TabId}" Value="3">
<Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=Tab3Template}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<TabControl DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding TabId}">
<TabControl.ItemsSource>
<col:Hashtable>
<sys:String x:Key="1">one</sys:String>
<sys:String x:Key="2">two</sys:String>
<sys:String x:Key="3">three</sys:String>
</col:Hashtable>
</TabControl.ItemsSource>
</TabControl>
What you are looking for is a DataTemplateSelector. It let's you choose a DataTemplate based on different criteria.
public class TaskListDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var element = container as FrameworkElement;
var vmBase = item as VMBase;
if (element != null && vmBase != null)
{
switch(vmBase.TabID)
{
case "Tab1": return element.FindResource("Tab1Template") as DataTemplate;
case "Tab2": return element.FindResource("Tab2Template") as DataTemplate;
default: return null;
}
}
}
}
You can read more about them in the docs or have a look at this tutorial.
I have XAML related question I have tried to research an answer in vain. I have commented the relevant questions to the XAML. It looks to me this questions is a more complex because of the way I try to arrange things.
Basically I have a main view model used in the TabControl headers and then in the content area I would show a list of items from the main view model. I just don't know how to to the binding. This is the main question. However, I suspect the next and ultimate objectives I have might factor in how to think about this, so I added them too. The rest of the code is for the sake of completeness.
<StackPanel>
<TabControl x:Name="mainsTabControl"
IsSynchronizedWithCurrentItem="True"
IsEnabled="True"
Visibility="Visible"
ItemsSource="{Binding Path=Mains}">
<!-- How to select a different background for the selected header? Note that the background color is "selected tab" if MainContentViewModel.IsActive is not TRUE.
If it is, a different color is chosen. Here this fact is just emulated with IsEnabled property due to well, multi-binding to the rescue (and a converter)? -->
<!-- Is there a clean way to use ICommand binding (RelayCommand) to check if it is OK to change the tab and if necessary, present a dialogue asking for the change? -->
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="IsEnabled" Value="{Binding IsActive}"/>
</Style>
</TabControl.ItemContainerStyle>
<!-- This binds to every item in the MainViewModel.Mains collection. Note the question about background color. -->
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<!-- This binding gives reference to the selected MainContentViewModel. -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Margin="10" DataContext="{Binding ElementName=mainsTabControl, Path=SelectedItem, Mode=OneWay}">
<ItemsControl ItemsSource="{Binding Path=CustomItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</StackPanel>
using GalaSoft.MvvmLight;
using System.Collections.ObjectModel;
using System.Linq;
namespace WpfDependencyInjection.ViewModel
{
public class MainContentViewModel: ViewModelBase
{
private ObservableCollection<CustomItemViewModel> customItems;
private mainContentDto MainContent { get; set; }
public string Name { get; }
public bool isActive;
public MainContentViewModel(Engine engine, mainContentDto mainContent)
{
MainContent = mainContent;
Name = MainContent.Name;
IsActive = true;
//The custom items belonging to this main content.
var customItems = engine.CustomItemContents.Where(i => i.MainContentId == MainContent.Id).Select(i => new CustomItemViewModel(engine, i));
CustomItems = new ObservableCollection<CustomItemViewModel>(customItems);
}
public ObservableCollection<CustomItemViewModel> CustomItems
{
get
{
return customItems;
}
set
{
customItems = value;
RaisePropertyChanged(nameof(CustomItems));
}
}
public bool IsActive
{
get
{
return isActive;
}
private set
{
isActive = value;
RaisePropertyChanged(nameof(IsActive));
}
}
}
}
public class CustomItemViewModel: ViewModelBase
{
private Engine Engine { get; }
private ItemTypeDto CustomItem { get; set; }
public string Name { get; }
public CustomItemViewModel(Engine engine, ItemTypeDto customItem)
{
Engine = engine;
CustomItem = customItem;
Name = customItem.Name;
}
}
namespace WpfDependencyInjection
{
public class Engine
{
public string Name { get; } = "EngineMan";
public List<mainContentDto> MainContents { get; set; } = new List<mainContentDto>(new[]
{
new mainContentDto { Name = "Main One", Id = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E1"), Version = 1 },
new mainContentDto { Name = "Main Two", Id = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E2"), Version = 1 }
});
public List<ItemTypeDto> CustomItemContents { get; set; } = new List<ItemTypeDto>(new ItemTypeDto[]
{
new ItemType1Dto { MainContentId = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E1"), Name = "ItemType1Dto I", Id = Guid.NewGuid(), Version = 1 },
new ItemType2Dto { MainContentId = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E1"), Name = "ItemType2Dto I", Id = Guid.NewGuid(), Version = 1 },
new ItemType2Dto { MainContentId = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E2"), Name = "ItemType2Dto 2", Id = Guid.NewGuid(), Version = 1 }
});
public Engine()
{
}
}
}
<edit: The binding solved partially, though not the ICommand part.
Try this:
<TabControl x:Name="mainsTabControl"
IsEnabled="True"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Mains}"
SelectedItem="0"
Visibility="Visible">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter ContentSource="Header" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="HotPink" />
</Trigger>
</Style.Triggers>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:MainContentViewModel}">
<Button Background="{x:Null}"
Command="{Binding SomeCommand}"
Content="{Binding Name}"
FocusVisualStyle="{x:Null}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type local:MainContentViewModel}">
<ItemsControl Margin="10"
VerticalAlignment="Top"
ItemsSource="{Binding Path=CustomItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:CustomItem}">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Command:
There may be cleaner ways but here we bind IsSelected for the TabItem to the IsSelected property of the viewmodel. This enables having a command that asks if it is ok to navigate and sets IsSelected to true if it is.
Background:
We also retemplate the tabitem so that background works as we want. If you check with Snoop WPF inserts an extra border when the item is selected.
Side note 1:
Don't put the TabControl into a StackPanel like that. A StackPanel sizes to content and will kill scrolling and draw outside the control. Also it comes with a cost, a deep visual tree is not cheap. Same in the ItemTemplate and the other places. In fact StackPanel is rarely right for anything :)
Side note 2:
If you specify DataType in your DataTemplate you get intellisense and some compiletime checking.
I have a WPF application based on MVVM.
It has paramaters that can be set. A paramater can be a list of strings in a checkbox or a double in a numupdown. Every device has its own parameters, so the parameters are dynamically loaded. Thats why i use a DataTemplateSelector.
Xaml code
<ItemsControl ItemsSource="{Binding ParameterWrapperList}" ItemTemplateSelector="{StaticResource propertyDataTemplateSelector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
The templates are in a resource dictionary:
<DataTemplate x:Key="doubleTemplate">
<Grid Visibility="{Binding Parameter.ParameterModel.IsVisible, Converter={StaticResource Visibility}}">
<Grid.RowDefinitions>
<RowDefinition Height="{Binding Path=RowHeight, Mode=OneWay}"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Style="{StaticResource HeaderBorderStyle}" Width="{Binding Width}" Visibility="{Binding HeaderVisibility, Converter={StaticResource Visibility}}">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Parameter.DisplayName}"/>
</StackPanel>
</Border>
<Border Grid.Row="1" Style="{StaticResource ItemBorderStyle}" Width="{Binding Width}">
<telerik:RadNumericUpDown Name="nudParameterValue" Value="{Binding Path=ParameterValue, Mode=TwoWay}" Minimum="{Binding Parameter.ParameterModel.MinimumValue}" Maximum="{Binding Parameter.ParameterModel.MaximumValue}" NumberDecimalDigits="{Binding Parameter.ParameterModel.NumberDecimalDigits}" SmallChange="{Binding Parameter.ParameterModel.SmallChanges}"/>
</Border>
</Grid>
</DataTemplate>
<DataTemplate x:Key="stringTemplate">
<Grid Visibility="{Binding Parameter.ParameterModel.IsVisible, Converter={StaticResource Visibility}}">
<Grid.RowDefinitions>
<RowDefinition Height="{Binding Path=RowHeight, Mode=OneWay}"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Style="{StaticResource HeaderBorderStyle}" Width="{Binding Width}" Visibility="{Binding Path=HeaderVisibility, Converter={StaticResource Visibility}}">
<Label Content="{Binding Parameter.DisplayName}"/>
</Border>
<Border Grid.Row="1" Style="{StaticResource ItemBorderStyle}" Width="{Binding Width}" >
<ComboBox ItemsSource="{Binding Path=Parameter.ParameterModel.PossibleValues}" SelectedIndex="{Binding ParameterValue, Mode=TwoWay}">
</ComboBox>
</Border>
</Grid>
</DataTemplate>
<configControl:PropertyDataTemplateSelector
StringTemplate="{StaticResource stringTemplate}"
DoubleTemplate="{StaticResource doubleTemplate}"
x:Key="propertyDataTemplateSelector"/>
I wrote the code for the DataTemplateSelector.
public class PropertyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate DoubleTemplate { get; set; }
public DataTemplate StringTemplate { get; set; }
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
DeviceModel.ParameterType devFunct = ((ParameterWrapper)item).Parameter.ParameterModel.Type;
switch (devFunct)
{
case DeviceModel.ParameterType.Double:
return DoubleTemplate;
case DeviceModel.ParameterType.String:
return StringTemplate;
default:
return null;
}
return null;
}
}
When the applications opens all the values are set fine. But when i update ParameterValue, it isn't updated on view. (when i output the values in console window, the values are set fine (but not in the GUI)).
What should i do to make this work.
Edit
This is the code it's bound to. Every function has a checkbox and when you change the parameter of one, all other parameters that are checked need to change.
public double ParameterValue
{
get
{
return _parameter.ParameterValue;
}
set
{
Parameter.ParameterValue = value;
Console.WriteLine("ParameterValueChanged");
if (SetOthers)
{
if (_parentFunction.Checked)
{
foreach (var function in _parentFunction.FunctionWrapperList)
{
if (function.Checked)
{
foreach (var parameterWrapper in function.ParameterWrapperList)
{
if (parameterWrapper.Parameter != this.Parameter)
{
if (parameterWrapper.Parameter.ParameterModel == Parameter.ParameterModel)
{
Console.WriteLine(function.Function.Name + " " + function.Checked + " " + Parameter.ParameterValue);
parameterWrapper.SetValue(Parameter.ParameterValue);
}
}
}
}
}
}
}
Notify("ParameterValue");
}
}
private void SetValue(double parameterValue)
{
SetOthers = false;
Parameter.ParameterValue = parameterValue;
SetOthers = true;
}
The boolean SetOthers is so he won't get a stackoverflow and keeps setting itself.
Your DataTemplateSelector won't update the DataTemplate rendering your data because a DataTemplateSelector doesn't trigger on a property change. It only choose a way to display a data according to its type (yourData.GetType()).
What you want is a trigger:
<ItemsControl ItemsSource="{Binding ParameterWrapperList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate" Value="{StaticResource doubleTemplate}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Parameter.ParameterModel.Type}" Value="{x:Static local:DeviceModel.ParameterType.String}">
<Setter Property="ContentTemplate" Value="{StaticResource stringTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
I am working on displaying rows which are grouped on WPF DataGrid
Group Header which I am currently displaying is a TextBlock, and the text displayed on the TextBlock is not selectable.
How can I make TextBlock text selectable so that I can copy the value.
Following is the XAML.
I am using following code to Bind data to the grid and group the data.
Entity:
public class AverageCounter
{
public string CounterName { get; set; }
public string Role { get; set; }
public string RoleInstance { get; set; }
public decimal CounterAverageValue { get; set; }
}
Code to Bind and Group.
var results = new ListCollectionView(queryResultSet);
if (results.GroupDescriptions != null)
{
results.GroupDescriptions.Add(new PropertyGroupDescription("CounterName"));
}
dataGrid1.AutoGenerateColumns = true;
dataGrid1.ItemsSource = results;
XAML:
<Window x:Class="CheckPerfromanceCounters.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CheckPerfromanceCounters"
Title="MainWindow" Height="390" Width="878">
<Window.Resources>
<local:AvgConverter x:Key="avgConverter"/>
</Window.Resources>
<Grid>
<Button Content="Refresh" Height="23" HorizontalAlignment="Left" Margin="769,328,0,0" Name="button1"
VerticalAlignment="Top" Width="75" Click="Button1Click" />
<DataGrid AutoGenerateColumns="True" Height="310" HorizontalAlignment="Left" Margin="12,12,0,0"
Name="dataGrid1" VerticalAlignment="Top" Width="832">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=CounterName}" />
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander>
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" FontStyle="Italic"/>
<TextBlock><Bold> - Average: </Bold></TextBlock>
<TextBlock Text="{Binding Converter={StaticResource avgConverter}}" />
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>
</Grid>
</Window>
if you need anyother information please let me know.
Just use TextBox and make it readonly, then you can change the TextBox.Style to make it looks like TextBlock.
Perhaps something like this
<TextBox IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
TextWrapping="Wrap" />
Use TextBox with IsReadOnly ="True" but also set the binding mode to oneWay :
Text="{Binding Converter={StaticResource avgConverter}, Mode=OneWay}"