How to add a DataTemplate to CollectionContainer? - c#

Here is my specific scenario.
The Window resources code:
...
<Window.Resources>
<ResourceDictionary>
<CollectionViewSource x:Key="AdditionalStringData" Source="{Binding ViewModelObservableCollection_String}"/>
<CollectionViewSource x:Key="AdditionalCustomObjectData" Source="{Binding ViewModelObservableCollection_CustomObject}"/>
<ResourceDictionary.MergedDictionaries>
...
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
...
The part where I need to display the Collection:
...
<StackPanel>
<ItemsControl>
<ItemsControl.ItemsSource>
<CompositeCollection>
<TextBlock Text="{Binding ViewModelTextProperty}"/>
<Button Command="{Binding ViewModelRelayCommand}">Command</Button>
<CollectionContainer Collection="{Binding Source={StaticResource AdditionalStringData}}" />
<CollectionContainer Collection="{Binding Source={StaticResource AdditionalCustomObjectData}}" />
</CompositeCollection>
</ItemsControl.ItemsSource>
</ItemsControl>
</StackPanel>
...
The ViewModel (assume that it is binded correctly)
...
private string ViewModelTextProperty { get; set; } = "Sample Text";
public RelayCommand ViewModelRelayCommand { ... }
private ObservableCollection<string> ViewModelObservableCollection_String { get; set; } = new ObservableCollection<string>();
private ObservableCollection<CustomObject> ViewModelObservableCollection_CustomObject { get; set; } = new ObservableCollection<CustomObject>();
...
The Class CutomObject (it may not be needed to show):
...
public class CustomObject
{
public string firstString;
public string secondString;
public CustomObject()
{
...
}
...
}
...
Assume that the ObservableCollections has proper contents.
My question is: How can I display the collection properly?
Here is the criteria:
On the first line, there will be a TextBlock with a Text inside it that says "Sample Text"
Next is a button with a label "Command"
Next lines (as many as ViewModelObservableCollection_String items) are TextBlocks. Its text should be the value of the individual item of ViewModelObservableCollection_String.
Next lines (as many as ViewModelObservableCollection_CustomObject items) are TextBoxes. Its text should be the value of the individual item of ViewModelObservableCollection_CustomObject (concatenation of firstString and secondString).
As you can see, the content of the StackPanel is a merge of more than one Collection with different DataTemplate.
Please ask for clarification if something is not clear enough.

Use DataTrigger inside ItemTemplate to change ControlTemplate of Control used, while comparing Type. For this use a converter which would return the type.
or,
Use ContentControl as ItemTemplate.
Define DataTemplate specifying DataType in it. ContentControl will automatically pick appropriate DataTemplate for its ContentTemplate.
Second Approach (Recommended)
<Window.Resources>
<ResourceDictionary>
...
<DataTemplate DataType="{x:Type sys:String}">
<TextBlock Background="ForestGreen" Text="{Binding .}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:CustomObject}">
<StackPanel Orientation="Horizontal">
<TextBlock Background="Red" Text="{Binding firstString}"/>
<TextBlock Background="Red" Text="{Binding secondString}"/>
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<ItemsControl>
...
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding .}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
...
</ItemsControl>

Related

How to fill each tab of the tab control's itemslist with one user control dynamically from the mainviewmodel

My MainView contains a TabControl with an ItemTemplate and a ContentTemplate.
The TabControl's ItemsSource is bound to the Property ObservableCollection<TabViewModel> TabCollection in my MainViewModel.
TabViewModel:
namespace LuxUs.ViewModels
{
public class TabViewModel
{
public string Name { get; set; }
public object VM {get; set;}
public TabViewModel(string name)
{
Name = name;
}
public TabViewModel(string name, object vm)
{
Name = name;
VM = vm;
}
}
}
I want to create the tabs with their tab's header AND content dynamically from the MainViewModel like this...:
MainViewModel:
using System.Collections.ObjectModel;
namespace LuxUs.ViewModels
{
public class MainViewModel : ObservableObject, IPageViewModel
{
public ObservableCollection<TabViewModel> TabCollection { get; set; }
public MainViewModel()
{
TabCollection = new ObservableCollection<TabViewModel>();
TabCollection.Add(new TabViewModel("Dachdefinition", new DachdefinitionViewModel()));
TabCollection.Add(new TabViewModel("Baukörperdefinition"));
TabCollection.Add(new TabViewModel("Fassade"));
TabCollection.Add(new TabViewModel("Raumdefinition"));
TabCollection.Add(new TabViewModel("Treppenloch | Galerieöffnung"));
}
}
}
View:
<UserControl x:Class="LuxUs.Views.MainView"
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:local="clr-namespace:LuxUs.Views"
xmlns:models="clr-namespace:LuxUs.Models"
xmlns:vm="clr-namespace:LuxUs.ViewModels"
mc:Ignorable="d">
<Grid>
<Grid>
<TabControl Style="{DynamicResource TabControlStyle}" ItemsSource="{Binding TabCollection}" >
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<UserControl>
<ContentControl Content="{Binding VM}" />
</UserControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Grid>
</UserControl>
... but the content of each tab won't show. Instead, I get this text. The ViewModel ist the correct one but it should load the view instead of showing this text, of course:
The first tab's ViewModel DachdefinitionViewModel has only an empty constructor:
using System.Collections.ObjectModel;
namespace LuxUs.ViewModels
{
public sealed class DachdefinitionViewModel : ObservableObject
{
public DachdefinitionViewModel()
{
}
}
}
And here is its view Dachdefinition.xaml:
<UserControl x:Class="LuxUs.Views.Dachdefinition"
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:local="clr-namespace:LuxUs.Views"
xmlns:vm="clr-namespace:LuxUs.ViewModels"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:DachdefinitionViewModel></vm:DachdefinitionViewModel>
</UserControl.DataContext>
<Grid Margin="50">
...
...
...
</Grid>
</UserControl>
Is the binding correct here or do I need to bind differently? Why is the view not showing up inside the first tab?
you need to connect view model with correct view via DataTemplate.
DataTemplate provides visual representation for data type which doesn't have it (your view model). If you don't specify DataTemplate, you will get default one: TextBlock with string representation of object (result of ToString() method, default to type name).
<Grid>
<Grid.Resources>
<DataTemplate DataType="{x:Type vm:DachdefinitionViewModel}">
<views:Dachdefinition />
</DataTemplate>
</Grid.Resources>
<TabControl Style="{DynamicResource TabControlStyle}"
ItemsSource="{Binding TabCollection}" >
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<UserControl>
<ContentControl Content="{Binding VM}" />
</UserControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
Your TabControl declaration should look like this:
<TabControl ItemsSource="{Binding TabCollection}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:DachdefinitionViewModel}">
<local:Dachdefinition/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding Name}"/>
<Setter Property="Content" Value="{Binding VM}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
And the Dachdefinition UserControl must not set its own DataContext property, because the DataContext value is supposed to be inherit from the control's parent element, i.e. the TabItem.
<UserControl x:Class="LuxUs.Views.Dachdefinition" ...>
<!--
do not set UserControl.DataContext here
-->
<Grid Margin="50">
...
</Grid>
</UserControl>
Yes there is a problem in databinding.
at
<ContentControl Content="{Binding VM}" />
This line would just display ToString() value of the object bound to
it. (VM in this case).
Instead you can try using ContentTemplateSelection where you can choose the type of ContentTemplate in run time based on the type of object bound to it.
class TabContentTemplateSelector:DataTemplateSelector
{
public DataTemplate DefaultTemplate { get; set; }
public DataTemplate DachdeTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is TabViewModel tabViewModel)
{
if (tabViewModel.VM != null && tabViewModel.VM is DachdefinitionViewModel)
{
return DachdeTemplate;
}
else
{
return DefaultTemplate;
}
}
return base.SelectTemplate(item, container);
}
}
<DataTemplate x:Key="DachdeTemplate">
</DataTemplate>
<DataTemplate x:Key="SomeOtherTemplate">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<local:TabContentTemplateSelector x:Key="myTabContentTemplateSelector"
DachdeTemplate="{StaticResource DachdeTemplate}"
DefaultTemplate="{StaticResource
SomeOtherTemplate}"
/>
</UserControl.Resources>
<Grid>
<TabControl ItemsSource="{Binding TabCollection}"
ContentTemplateSelector="{StaticResource
myTabContentTemplateSelector}" >
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
https://www.c-sharpcorner.com/UploadFile/41e70f/dynamically-selecting-datatemplate-for-wpf-listview-way-1/
Check this for how to dynamically change template.. In the template you can use any kind of user control.

Selecting DataTemplate from Window Resource in ViewModel with MVVM

I have a View where a small part of the window that displays the details of the item a user clicks on. The format of these details changes, so my original implementation had a hide/show logic for the different kinds of items:
<Grid Name="Details1" Visibility="Collapsed">
<TextBox Name="Details_Field1" />
</Grid>
<Grid Name="Details2" Visibility="Visible">
<TextBox Name="Details_Field2" />
<TextBox Name="Details_Field3" />
</Grid>
<Grid Name="Details3" Visibility="Collapsed">
<TextBox Name="Details_Field4" />
<TextBox Name="Details_Field5" />
<DataGrid Name="Details_DataGrid1 />
</Grid>
Now, I want to make this 'less bad'. My strategy was to make each of these Grids it's own DataTemplate, and manage state like so:
View:
<Window.Resource>
<DataTemplate x:Key="Details_Template1>
<Grid Name="Details1">
<TextBox Name="Details_Field1" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="Details_Template2>
<Grid Name="Details2">
<TextBox Name="Details_Field2" />
<TextBox Name="Details_Field3" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="Details_Template3>
<Grid Name="Details3">
<TextBox Name="Details_Field4" />
<TextBox Name="Details_Field5" />
<DataGrid Name="Details_DataGrid1 />
</Grid>
</DataTemplate>
</Window.Resources>
....
<Grid Name="DetailsGoHere">
<ContentControl ContentTemplate="{Binding DetailsDisplay}" />
</Grid>
ViewModel:
private DataTemplate _detailsDisplay;
public DataTemplate DetailsDisplay
{
get => _detailsDisplay;
private set => RaisePropertyChangedEvent(ref _detailsDisplay, value);
}
....
private void Item_OnClick()
{
// Pseudocode! How do I reference Details_Template1 as a resource?
DetailsDisplay = MyView.Details_Template1;
}
As the comment implies, I'm not sure how to reference the DataTemplates in my Window.Resource block from my ViewModel, so my question is twofold:
1: Is this a good solution to this kind of a problem?
2: If so, how do I reference an item from my Window.Resource block in my ViewModel?
DataTemplateSelector are used to switch DataTemplate in WPF.
Kindly refer this link http://www.wpftutorial.net/datatemplates.html for detailed implementation.
PropertyDataTemplateSelector-
public class PropertyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultnDataTemplate { get; set; }
public DataTemplate BooleanDataTemplate { get; set; }
public DataTemplate EnumDataTemplate { get; set; }
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
DependencyPropertyInfo dpi = item as DependencyPropertyInfo;
if (dpi.PropertyType == typeof(bool))
{
return BooleanDataTemplate;
}
if (dpi.PropertyType.IsEnum)
{
return EnumDataTemplate;
}
return DefaultnDataTemplate;
}
}
View -
<Window x:Class="DataTemplates.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:DataTemplates"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Window.Resources>
<!-- Default DataTemplate -->
<DataTemplate x:Key="DefaultDataTemplate">
...
</DataTemplate>
<!-- DataTemplate for Booleans -->
<DataTemplate x:Key="BooleanDataTemplate">
...
</DataTemplate>
<!-- DataTemplate for Enums -->
<DataTemplate x:Key="EnumDataTemplate">
...
</DataTemplate>
<!-- DataTemplate Selector -->
<l:PropertyDataTemplateSelector x:Key="templateSelector"
DefaultnDataTemplate="{StaticResource DefaultDataTemplate}"
BooleanDataTemplate="{StaticResource BooleanDataTemplate}"
EnumDataTemplate="{StaticResource EnumDataTemplate}"/>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding}" Grid.IsSharedSizeScope="True"
HorizontalContentAlignment="Stretch"
ItemTemplateSelector="{StaticResource templateSelector}"/>
</Grid>

XAML: Simple ItemsControl Binding to TextBox does not work: how to set the source property?

I have an ObservableCollection that I try to bind to a list of text boxes. The textboxes do show but the content of the text does not.
The XAML:
<Grid>
<Grid.RowDefinitions >
<RowDefinition />
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding Path=ListOfMessages}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=Message, ElementName=ListOfMessages}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
The Code:
In the ViewModel:
public ObservableCollection<ApplicationLog> ListOfMessages { get; set; }
In the Model:
public class ApplicationLog
{
public string Code { get; set; }
public string Message { get; set; }
}
When I run this, the app shows the text boxes (for example 4 text boxes, one below the other), but the text in the text boxes (ie the Message property) is not shown. I think my Binding expression for the Text Box is wrong.
Context: I am new to XAML and WPF. More generally: how does one debug Binding issues similar to this one.
Thanks.
Remove ElementName=ListOfMessages. The DataContext for each item will be the items in the ListOfMessages bound to the ItemsSource.
<Grid>
<Grid.RowDefinitions >
<RowDefinition />
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding Path=ListOfMessages}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- DataContext will be ListOfMessages[0], [1], ..., [n] -->
<TextBox Text="{Binding Path=Message}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
ElementName is used to bypass the DataContext and point to a specific named item in your scoped XAML.

List of objects and ComboBoxes

Here's my problem:
I've got these classes:
public class CsvField
{
public string Content { get; set; }
public CsvField(string content)
{
Content = content;
}
}
public class CsvLine
{
public List<CsvField> Fields = new List<CsvField>();
public int LineNumber;
}
public static class Settings
{
public static List<string> Tags = new List<string>();
public static CsvLine AllHeaders = new CsvLine();
}
What I want to do, is display the ListBox containing every member of Settings.AllHeaders.Fields and a ComboBox containing all members of Settings.Tags list (placed horizontally - a member of AllHeaders on the left and a ComboBox next to it). So if I had 4 headers, I would get a List of those 4 headers and 4 ComboBoxes, each of them next to individual header. Each of these ComboBoxes would contain a list of tags.
So, I defined a DataTemplate:
<Window x:Class="CSV_To_Tags_App.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:CSV_To_Tags_App"
Title="Window2" Height="435" Width="566">
<Window.Resources>
<DataTemplate DataType="{x:Type loc:CsvField}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="HeaderTextBlock" HorizontalAlignment="Left" TextWrapping="Wrap"
VerticalAlignment="Top" Text="{Binding Content}"
/>
<ComboBox HorizontalAlignment="Right" VerticalAlignment="Top" Width="120"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<Label Content="Available headers" HorizontalAlignment="Left"
VerticalAlignment="Top"/>
<ListBox x:Name="HeadersListtListBox" HorizontalAlignment="Left"
Height="254" Margin="36,104,0,0" VerticalAlignment="Top" Width="452"
ItemsSource="{Binding}"/>
</Grid>
</Window>
Now, XAML code above is incomplete, because I don't know how to:
1. Bind TextBlock to Settings.AllHeaders.Fields.Content
2. Bind ComboBox to Tags List
The link provided by #MD's does provide the solution to what you are asking in your question, but you have to rearrange things a little bit to see it.
The XAML below will give you what you are asking for, assuming your class that you are binding to is set up properly for data binding (implementing the INotifyPropertyChanged interface). There are lots of examples on this site of how to properly implement that portion.
<Window x:Class="CSV_To_Tags_App.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:CSV_To_Tags_App"
Title="Window2" Height="435" Width="566">
<Grid>
<StackPanel Orientation="Horizontal">
<ItemsControl ItemsSource="{Binding Path=Settings.AllHeadings.Fields}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Content}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding Path=Settings}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=Tags}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</Window>

Binding a CollectionViewSource to a ListBox

For some reason I cannot get my ListBox to display data from my CollectionViewSource. Here is the code...
public class AppTest
{
public int Priority { get; set; }
public string TestName { get; set; }
}
public class AppTestProvider
{
public List<AppTest> GetAppTests()
{
return new List<AppTest>()
{
new AppTest() { Priority=1, TestName = "Application Setup" },
new AppTest() { Priority=2, TestName = "File System Permissions" }
};
}
}
... and now the Xaml...
<Window.Resources>
<ObjectDataProvider x:Key="AppTests" ObjectType="{x:Type Application:AppTestProvider}" MethodName="GetAppTests" />
<CollectionViewSource x:Key="cvs" Source="{Binding AppTests}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Priority" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<ListBox x:Name="TestList" ItemsSource="{Binding Source={StaticResource cvs}}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TestName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
If I change the ItemsSource of the ListBox to look like this (getting data from the ObjectDataSource and not the CVS) it displays the data albeit not sorted...
<ListBox x:Name="TestList" ItemsSource="{Binding Source={StaticResource AppTests}}">
I'm sure this must be something pretty simple. I just cannot seem to get it to work!
Replace this <CollectionViewSource x:Key="cvs" Source="{Binding AppTests}">
with <CollectionViewSource x:Key="cvs" Source="{StaticResource AppTests}">.
You are referring to resource defined in XAML so you need to use StaticResource instead of Binding to refer to ObjectDataProvider just like you are doing in later approach to set ItemsSource of your listBox.

Categories