Retrieve View info from DataTemplateSelector - c#

maybe this is a trivial question for many of you...
My app has a TabControl defined as:
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}">
<!--Bind the SelectionChanged event of the tab-->
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectedChangedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<!--This is How tab will look-->
<TabControl.ItemTemplate>
<DataTemplate>
<DockPanel>
<Button Name="BtnCloseTab"
DockPanel.Dock="Right"
Margin="5,0,0,0"
Padding="0"
Command="{Binding RelativeSource=
{RelativeSource FindAncestor, AncestorType={x:Type TabControl}},
Path=DataContext.CloseTabCommand}">
<Image Source="/EurocomCPS;component/Images/closeTab.png" Height="11" Width="11"></Image>
</Button>
<TextBlock Text="{Binding Header}" />
</DockPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<!--This will be the content for the tab control-->
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl
ContentTemplateSelector="{StaticResource TemplateSelector}"
Content="{Binding}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
In the window ViewModel I have the following prop:
private ObservableCollection<Tab> _Tabs;
public CPSViewModel()
{
_Tabs = new ObservableCollection<Tab>();
}
public ObservableCollection<Tab> Tabs
{
get { return _Tabs;}
private set
{
_Tabs = value;
this.RaisePropertyChanged("Tabs");
}
}
Now, when a new Tab is created, the following DataTemplateSelector is called:
class TemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item != null)
{
string templateFile = string.Format("Templates/{0}",
Properties.Settings.Default.AppId + ".tmpl");
if (File.Exists(templateFile))
{
FileStream fs = new FileStream(templateFile, FileMode.Open);
DataTemplate template = XamlReader.Load(fs) as DataTemplate;
return template;
}
}
return null;
}
}
The DataTemplate is based on the XmlDataProvider and here I need to "inform" the Template which xml file it has to load because it is different for every tab:
<DataTemplate
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DataTemplate.Resources>
<local:StringToBoolConverter x:Key="StringToBoolConverter" />
<local:StringToIntConverter x:Key="StringToIntConverter" />
<XmlDataProvider x:Key="dataProvider" XPath="func/parametri/param/BLOCKS"/>
</DataTemplate.Resources>
<Grid>
.... controls ....
</Grid>
</DataTemplate>
Is there a way to do it?
EDIT
Substantially what I have to do is to have access to my Tab class into the TemplateSelector.
Regards,
Daniele.

if you could define your tabs like
public class TabFirst:ITab {}
public class TabSecond:ITab {}
public class TabBlup:ITab {}
viewmodel
public ObservableCollection<ITab> Tabs
{
get { return _Tabs;}
private set
{
_Tabs = value;
this.RaisePropertyChanged("Tabs");
}
}
you could get rid of the DataTemplateSelector and just definfe your datatemplates in your resources
<DataTemplate DataType="{x:Type local:TabFirst}">
<view:TabFirstView />
<DataTemplate/>
<DataTemplate DataType="{x:Type local:TabSecond}">
<view:TabSecondView />
<DataTemplate/>
and your content control would be just
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding}" />
</DataTemplate>
</TabControl.ContentTemplate>

Related

x:Bind ViewModel RelayCommand to a command inside DataTemplate

I'm trying to bind a IAsyncRelayCommand (MVVM Toolkit) from my view model using x:Bind within a DataTemplate.
This is my view model:
public class BuildingViewModel : ObservableObject
{
public ObservableCollection<ObservableProjectItem> ProjectItems { get; } = new ObservableCollection<ObservableProjectItem>();
public IAsyncRelayCommand AddProjectItemCommand { get; }
public BuildingViewModel()
{
AddProjectItemCommand = new AsyncRelayCommand<ContentDialog>(async (dialog) => await AddProjectItem(dialog));
}
private async Task AddProjectItem(ContentDialog dialog)
{
// Do something
}
}
And this my view (XAML):
<TreeView ItemsSource="{x:Bind ViewModel.ProjectItems}">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="model:ObservableProjectItem">
<TreeViewItem ItemsSource="{x:Bind Children}">
<TextBlock Text="{x:Bind Name}" />
<TreeViewItem.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem Command="{HERE I WANT BIND THE COMMAND}" />
<MenuFlyoutItem Text="Löschen" Icon="Delete" />
</MenuFlyout>
</TreeViewItem.ContextFlyout>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The view have the following code behind:
public sealed partial class BuildingView : Page
{
public BuildingView()
{
this.InitializeComponent();
}
internal BuildingViewModel ViewModel = Ioc.Default.GetService<BuildingViewModel>();
}
My problem is {x:Bind ViewModel.AddProjectItemCommand} doesn't work. Within the DataTemplate the x:Bind scope is ObservableProjectItem. How can I navigate to my root ViewModel respectively bind my command declared in the view model?
Unfortunately, these kind of operations are not well supported by x:Bind (see this WinUI issue for more context). You could however use Binding to solve that problem. For this, you need to set x:Name on your page and use the ElementName property:
<Page x:Name="MyPage">
<TreeView ItemsSource="{x:Bind ViewModel.ProjectItems}">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="model:ObservableProjectItem">
<TreeViewItem ItemsSource="{x:Bind Children}">
<TextBlock Text="{x:Bind Name}" />
<TreeViewItem.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem Command="{Binding Path=AddProjectItemCommand, ElementName=MyPage}" />
<MenuFlyoutItem Text="Löschen" Icon="Delete" />
</MenuFlyout>
</TreeViewItem.ContextFlyout>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Page>
I have also encountered this problem.
My solution is this:
declare ICommand in ViewModel as static
use a construct {x:Bind Path=...}
Something like:
In ViewModel:
static public ICommand Command_TapColorSet { get; set; }
in XAML:
xmlns:models="using:TestProj.Models"
xmlns:viewmodels="using:TestProj.ViewModels"
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:ColorsSet">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="Tapped">
<Core:InvokeCommandAction Command="{x:Bind Path=viewmodels:OptionsViewModel.Command_TapColorSet}" CommandParameter="{Binding}" />
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>.
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
I realise that static ICommand looks a bit strange, but at least this approach works.

ListVIew of ItemsControl: Get the ListViewItem index after selecting one its item from ItemsControl

I have a ListView. Inside of this ListView there is ItemsControl and inside it there is second ItemsControl. Inside of the second ItemsControl there are TextBoxes.
ListView -> ItemsControl -> ItemsControl -> TextBox
Is there any chance that I would be able to get index of ListViewItem, which specific TextBox belongs to after clicking on this TextBox?
For example
I select a ListViewItem on index 0 but then I click on TextBox which belong to ListViewItem on index 2. In that case I would like to change value of SelectedGroupIndex from 0 to 2.
"Hello" strings are just for testing.
Thank you very much.
ViewModel
public class MainWindowViewModel
{
public ObservableCollection<ObservableCollection<ObservableCollection<ListViewString>>> AllTexts { get; set; }
public int SelectedGroupIndex { get; set; }
public ICommand AddGroup { get; private set; }
public ICommand AddColumn { get; private set; }
public ICommand TextBoxSelected { get; private set; }
public MainWindowViewModel()
{
this.AllTexts = new ObservableCollection<ObservableCollection<ObservableCollection<ListViewString>>>();
this.SelectedGroupIndex = -1;
this.AddGroup = new Command(this.AddGroupCommandHandler);
this.AddColumn = new Command(this.AddColumnCommandHandler);
this.TextBoxSelected = new Command(this.TextBoxSelectedCommandHandler);
}
private void AddGroupCommandHandler()
{
var tempColumn = new ObservableCollection<ListViewString>() {
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello") };
var tempGroup = new ObservableCollection<ObservableCollection<ListViewString>>();
tempGroup.Add(tempColumn);
this.AllTexts.Add(new ObservableCollection<ObservableCollection<ListViewString>>(tempGroup));
}
private void AddColumnCommandHandler()
{
if (this.SelectedGroupIndex >= 0 && this.SelectedGroupIndex < this.AllTexts.Count)
{
var tempColumn = new ObservableCollection<ListViewString>() {
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello") };
this.AllTexts[this.SelectedGroupIndex].Add(tempColumn);
}
}
private void TextBoxSelectedCommandHandler()
{
// TODO: Change SelectedItem of ListView
// this.SelectedGroupIndex = ...;
}
private ListViewString GetListViewString(string text)
{
return new ListViewString { Value = text };
}
private string GetTextFromListViewString(ListViewString listViewString)
{
return listViewString.Value;
}
}
/// <summary>
/// Class used to show user Text in ListView.
/// Using this class fixes the issue that ObservableCollection didn't update
/// after user changed values of TextBoxes in GUI.
/// </summary>
public class ListViewString : DependencyObject
{
public string Value
{
get
{
return (string)GetValue(ValueProperty);
}
set
{
SetValue(ValueProperty, value);
}
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(string), typeof(ListViewString), new PropertyMetadata(string.Empty));
}
View:
<Window.Resources>
<ResourceDictionary>
<local:MainWindowViewModel x:Key="vm" />
</ResourceDictionary>
</Window.Resources>
<Grid Margin="10,10,10,10" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="300" />
<RowDefinition />
</Grid.RowDefinitions>
<ListView Grid.Row="0"
ItemsSource="{Binding AllTexts, Source={StaticResource vm}, Mode=TwoWay}"
Background="Blue"
SelectedIndex="{Binding SelectedGroupIndex, Source={StaticResource vm}}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Value}"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Center"
Width="100" Height="40">
<TextBox.InputBindings>
<MouseBinding Gesture="LeftClick"
Command="{Binding TextBoxSelected, Source={StaticResource vm}}" />
</TextBox.InputBindings>
</TextBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,20,0,0">
<Button Content="Add Group" Width="120" Height="30"
Command="{Binding AddGroup, Source={StaticResource vm}}" />
<Button Content="Add Column" Margin="20,0,0,0" Width="120" Height="30"
Command="{Binding AddColumn, Source={StaticResource vm}}" />
<TextBlock Width="120" Height="30" FontSize="20" Margin="20,0,0,0"
Text="{Binding SelectedGroupIndex, Source={StaticResource vm}}" />
</StackPanel>
</Grid>
Actually what you need is to pass the DataContext of your TextBox to the command as parameter, so use CommandParameter for it and implement your command with parameter:
<TextBox.InputBindings>
<MouseBinding Gesture="LeftClick"
Command="{Binding TextBoxSelected, Source={StaticResource vm}}"
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Mode=Self}}"/>
</TextBox.InputBindings>
So you will have an item from your items source collection as command parameter and can find the index of it.

Biding contextmenu menuitem visibility with a list

I have a ContextMenu attached to a ListBox who offer two options : create and delete element. I want to hide only the "Delete" element if the ListBox data is empty.
I've tried to bind the property "Visibility" with a variable in the view's code setting it to "Collapsed" or "Visible", but it didn't work.
XAML :
<ListBox ItemsSource="{Binding ElementList}"
SelectedItem="{Binding SelectedElement}"
SelectionChanged="ListBoxProjects_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Name="Add" Click="Add_Click" Header="Add element" />
<MenuItem Name="Delete" Click="Delete_Click"
HeaderStringFormat="Delete element {0}"
Header="{Binding SelectedElement.Name}"
Visibility="{Binding ElementContextMenuVisibility}" />
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
CS :
public partial class View : UserControl
{
private ViewModel _viewModel = ViewModel.Instance;
private Visibility _elementContextMenuVisibility { get; set; }
public Visibility ElementContextMenuVisibility
{
get { return _elementContextMenuVisibility; }
set { _elementContextMenuVisibility = value; }
}
public View()
{
InitializeComponent();
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_viewModel.ElementList != null && _viewModel.ElementList.Count > 0)
ElementContextMenuVisibility = Visibility.Visible;
else
ElementContextMenuVisibility = Visibility.Collapsed;
}
}
Thanks
You can achieve this with RelativeSource binding and no need of xaml.cs code.
XAML
<ListBox ItemsSource="{Binding ElementList}"
SelectedItem="{Binding SelectedElement}">
<ListBox.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Name="Add" Click="Add_Click" Header="Add element" />
<MenuItem Name="Delete" Click="Delete_Click"
HeaderStringFormat="Delete element {0}"
Header="{Binding SelectedElement.Name}"
Visibility="{Binding PlacementTarget.HasItems, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BooleanToVisibilityConverter}}" />
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>

Bind a VM to different views, depending on VM's properties

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.

Data bind an ItemsControl in a DataTemplate

I seem to have a simple data-binding problem, but can't figure out the right way to do it. There is a TabControl which defines two DataTemplate's, one for the tab header and one for the tab content.
The content template contains an ItemsControl. The ItemsControl tries to bind to a dynamically created ViewModel (ConnectionInfoVM).
When I display the UI, the binding just fails, but there is no error-message in the output about it.
How do I have to set up the DataContext and the binding so the binding works and the DataBuffer is actually displayed? Any help greatly appreciated.
ConnectionsControl:
<UserControl x:Class="XXXViewer.Views.ConnectionsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:XXXViewer.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TabControl Grid.Row="0" Name="TabDynamic" SelectionChanged="tabDynamic_SelectionChanged">
<TabControl.Resources>
<DataTemplate x:Key="TabHeader" DataType="TabItem">
<DockPanel>
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Header}" />
<Button Name="btnDelete" DockPanel.Dock="Right" Margin="5,0,0,0" Padding="0" Click="btnTabDelete_Click" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Name}">
<Image Source="{DynamicResource DeleteImg}" Height="11" Width="11"></Image>
</Button>
</DockPanel>
</DataTemplate>
<DataTemplate x:Key="TabContent" DataType="viewModels:ConnectionInfoVM">
<StackPanel>
<ScrollViewer Name="Scroller" Background="Black">
<StackPanel>
<TextBlock Text="This line gets printed" Foreground="White" FontFamily="Consolas"/>
<ItemsControl Name="ItemCtrl" ItemsSource="{Binding DataBuffer}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=.}" Foreground="White" FontFamily="Consolas"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</DataTemplate>
</TabControl.Resources>
</TabControl>
</Grid>
</UserControl>
ConnectionsControl code behind:
namespace XXXViewer.Views
{
public partial class ConnectionsControl : UserControl
{
private readonly ObservableCollection<TabItem> _tabItems = new ObservableCollection<TabItem>();
public ConnectionsControl()
{
InitializeComponent();
// bindings
TabDynamic.ItemsSource = _tabItems;
TabDynamic.DataContext = this;
}
// assume this gets called
private void AddTabItem(ConnectionInfoVM ci)
{
DataTemplate headerTemplate = TabDynamic.FindResource("TabHeader") as DataTemplate;
DataTemplate contentTemplate = TabDynamic.FindResource("TabContent") as DataTemplate;
// create new tab item
TabItem tab = new TabItem
{
Header = $"Tab {ci.ConnectionID}",
Name = $"T{ci.ConnectionID}",
HeaderTemplate = headerTemplate,
ContentTemplate = contentTemplate,
DataContext = ci
};
_tabItems.Insert(0, tab);
// set the new tab as active tab
TabDynamic.SelectedItem = tab;
}
}
}
ConnectionInfoVM:
namespace XXXViewer.ViewModels
{
public class ConnectionInfoVM : ViewModelBase
{
private readonly ObservableQueue<string> _dataBuffer = new ObservableQueue<string>();
public ObservableQueue<string> DataBuffer => _dataBuffer;
}
}
Screenshot of the tab that gets created:
resulting tab
You set the ContentTemplate but never the Content, so the ContentTemplate is never applied because it's applied only when there's Content set. Instead of DataContext = ci write Content = ci.
By the way the DataContext = ci was useless because the DataContext is already implicitely the object on which the DataTemplate is applied.
Edit
As you're using WPF, use and abuse of its core feature: bindings.
How I would have written your code (if I didn't use full MVVM compliant code):
Your XAML:
<TabControl Grid.Row="0" Name="TabDynamic"
ItemsSource="{Binding TabItems, Mode=OneWay}"
SelectionChanged="tabDynamic_SelectionChanged">
<TabControl.Resources>
<DataTemplate x:Key="TabHeader" DataType="TabItem">
<DockPanel>
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Header}" />
<Button Name="btnDelete" DockPanel.Dock="Right" Margin="5,0,0,0" Padding="0" Click="btnTabDelete_Click" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Name}">
<Image Source="{DynamicResource DeleteImg}" Height="11" Width="11"></Image>
</Button>
</DockPanel>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate DataType="viewModels:ConnectionInfoVM">
<TabItem Header="{Binding ConnectionID, Mode=OneWay}"
Name="{Binding ConnectionID, Mode=OneWay}"
HeaderTemplate="{StaticResources TabHeader}">
<StackPanel>
<ScrollViewer Name="Scroller" Background="Black">
<StackPanel>
<TextBlock Text="This line gets printed" Foreground="White" FontFamily="Consolas"/>
<ItemsControl Name="ItemCtrl" ItemsSource="{Binding DataBuffer}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=.}" Foreground="White" FontFamily="Consolas"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</TabItem>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
You cs code become much simpler:
namespace XXXViewer.Views
{
public partial class ConnectionsControl : UserControl
{
private readonly ObservableCollection<ConnectionInfoVM> _tabItems = new ObservableCollection<ConnectionInfoVM>();
public ObservableCollection<ConnectionInfoVM> TabItems {get {return _tabItems;}}
public ConnectionsControl()
{
InitializeComponent();
// bindings
//TabDynamic.ItemsSource = _tabItems;
TabDynamic.DataContext = this;
}
// assume this gets called
private void AddTabItem(ConnectionInfoVM ci)
{
TabItems.Add(ci);
}
}
}
I noted while re-reading your code that you were probably confused about binding in code-behind.
Your code TabDynamic.ItemsSource = _tabItems; is not a binding, it will only set it once.
Anyway, I suggest you read a bit about MVVM. The TabItems should be in a ViewModel class instead of being in code-behind.
The Tabcontrol as per your coding does not contain in it's DataContext the viewmodel but the control; so we need to find a control or something else which holds the VM. It does not appear that the page holds the VM in its DataContext either.
I recommend that one route is to use the TabControl's Tag property to hold the VM such as specifying it in code behind as such:
TabDynamic.ItemsSource = _tabItems;
TabDynamic.DataContext = this;
TabDynamic.Tag = {Wherever you are keeping your VM at this time its not clear in your code example};
Then you can specify Tag from the template binding by specifying the TabControls' name as such:
<ItemsControl Name="ItemCtrl"
ItemsSource="{Binding Tag.DataBuffer,
ElementName=TabDynamic}">

Categories

Resources