How to use WPF DataTemplates only on a part of the layout - c#

In a MVVM environment I am using a list box where I am visualizing different data types in a different way but having some style elements in common. A minimal sample looks like this:
interface IPropertyItem
{
string Description { get; }
}
class PropertyBoolItem : IPropertyItem
{
public string Description { get; }
public bool Value { get; set; }
}
class PropertyStringItem : IPropertyItem
{
public string Description { get; }
public string DifferentValue { get; set; }
}
<ListBox>
<ListBox.Resources>
<!-- A bool item -->
<DataTemplate DataType="{x:Type types:PropertyBoolItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="130"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text{Binding Description}/>
<CheckBox Grid.Column="3" IsChecked="{Binding Value}"/>
</Grid>
</DataTemplate>
<!-- A string item -->
<DataTemplate DataType="{x:Type types:PropertyStringItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="130"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text{Binding Description}/>
<TextBox Grid.Column="3" Text="{Binding DifferentValue, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
<!-- and so on... -->
</ListBox.Resources>
</ListBox>
Everythings is working so far. But I was wondering if there is a more elegant way to implement this as big parts of the markup are always the same and only some parts (as the checkbox or the textbox) are dependent on the data type.
Thanks in advance!

Not sure if this qualifies as "more elegant", but it is less XAML, and you wouldn't have to bother with the complex ListBoxItem Template.
<ListBox ...>
<ListBox.Resources>
<!-- A bool item -->
<DataTemplate DataType="{x:Type types:PropertyBoolItem}">
<CheckBox IsChecked="{Binding Value}"/>
</DataTemplate>
<!-- A string item -->
<DataTemplate DataType="{x:Type types:PropertyStringItem}">
<TextBox Text="{Binding DifferentValue}"/>
</DataTemplate>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="130"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Description}"/>
<ContentPresenter Grid.Column="3" Content="{Binding}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Related

WPF Expander - Button in Header

I am building an expander control where I want to have an additional button in the header (additional to the toggle). When I add a button on the <Expander.Header>, the binded command is not triggered when it is clicked.
I have tried creating a style/template for the Expander and Expander.Header so that the Expander's toggle is only triggered on its click (rather than the entire header). I did this thinking that it is some sort of heirarchical issue where the header is consuming the click event rather than my button, but it did not work out as I had hoped...
Is there a specific way I should modify the template to allow for a button to be placed in the header and have the button's command executed?
xaml of my expander control - all bound to my VM, can confirm the binding works correctly:
<Expander Background="White">
<Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
HorizontalAlignment="center"
FontSize="16"
Margin="5 0 5 0"
Text="{Binding Name}"/>
<Button
Grid.Column="2"
Width="15"
Height="15"
Margin="10 10 10 10"
Command="{Binding OpenButtonCommand}"/>
</Grid>
</Expander.Header>
<TextBlock Margin="30 0 0 0">This is a test sub component</TextBlock>
</Expander>
Thank you
Actually it should be working;
Tested with following VM:
using Prism.Commands;
using Prism.Mvvm;
using System;
namespace WpfApp2
{
public class MainViewModel : BindableBase
{
public DelegateCommand OpenButtonCommand { get; private set; }
private string m_Name = "Test Name";
public string Name
{
get { return m_Name; }
set
{
m_Name = value;
RaisePropertyChanged(nameof(Name));
}
}
public MainViewModel()
{
OpenButtonCommand = new DelegateCommand(OpenButtonCommandExecute);
}
private void OpenButtonCommandExecute()
{
Name = "clicked";
}
}
}
and XAML:
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<Expander
Width="300"
HorizontalAlignment="Left"
Background="White">
<Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="1"
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="16"
Text="{Binding Name}"
TextAlignment="Left"
TextWrapping="Wrap" />
<Button
Grid.Column="2"
Width="15"
Height="15"
Margin="10,10,10,10"
Command="{Binding OpenButtonCommand}" />
</Grid>
</Expander.Header>
<TextBlock Margin="30,0,0,0">This is a test sub component</TextBlock>
</Expander>
</Grid>
Result:

Use data template based on property

Is it possible to control which DataTemplate a node used based on a property rather than the class type which is typically seen in wpf.
In my case I have a list of teams and I want to control the background of each team being listed in the listbox as well as a few other design elements such as displaying different logos based on which team. In my mind it seems easiest to just make a data template based on the team name.
How do you guys suggest I handle it. I don't want to create a class object for every entire team. However it would be ideal if a team doesn't have a design template that a default one gets used.
Either way if someone could put together a super simple example Id appreciate it considering im not sure how to do it .
From what you described, it seems to me that those property can be hold on the Team class and you could display the content based on them !, but since you might need something much complicated you could use a DataTemplateSelector basically what you need to do is :
First : In resources area define the DataTemplates that you need plus a default one, in case none of the teams names matche a difined template,
lets say something like that :
<Window.Resources>
<DataTemplate x:Key="DefaultnDataTemplate" DataType="{x:Type YourNameSpace:Team}">
<Grid Background="LightGreen">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="DefaultnDataTemplate"/>
<TextBlock Text="{Binding Id}" Grid.Row="1" Grid.Column="0"/>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Center" Grid.Row="1" Grid.Column="1"/>
<TextBlock Text="{Binding Matches}" Grid.Row="1" Grid.Column="2"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="TeamADataTemplate" DataType="{x:Type YourNameSpace:Team}">
<Grid Background="LightCoral">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="TeamADataTemplate"/>
<TextBlock Text="{Binding Id}" Grid.Row="1" Grid.Column="0"/>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Center" Grid.Row="1" Grid.Column="1"/>
<TextBlock Text="{Binding Matches}" Grid.Row="1" Grid.Column="2"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="TeamBDataTemplate" DataType="{x:Type YourNameSpace:Team}">
<Grid Background="LightBlue">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="TeamBDataTemplate"/>
<TextBlock Text="{Binding Id}" Grid.Row="1" Grid.Column="0"/>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Center" Grid.Row="1" Grid.Column="1"/>
<TextBlock Text="{Binding Matches}" Grid.Row="1" Grid.Column="2"/>
</Grid>
</DataTemplate>
</Window.Resources>
Second add a DataTemplateSelector class, the class will basically check the team name and return the appropriate DataTemplate:
public class TeamDataTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultnDataTemplate { get; set; }
public DataTemplate TeamADataTemplate { get; set; }
public DataTemplate TeamBDataTemplate { get; set; }
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
var teamName = (item as Team).Name;
switch (teamName)
{
case "TeamA":
return TeamADataTemplate;
case "TeamB":
return TeamBDataTemplate;
default:
return DefaultnDataTemplate;
}
}
}
Third, add an instance of that class to the StaticResources, and point it to the already defined DataTemplates :
<YourNameSpace:TeamDataTemplateSelector x:Key="TeamDataTemplateSelector" TeamADataTemplate="{StaticResource TeamADataTemplate}" TeamBDataTemplate="{StaticResource TeamBDataTemplate}" DefaultnDataTemplate="{StaticResource DefaultnDataTemplate}"/>
Finally call the TemplateSelector in your List:
<ListBox ItemsSource="{Binding Teams}" ItemTemplateSelector="{StaticResource TeamDataTemplateSelector}">
</ListBox>
here the model i am using in this sample :
public class Team
{
public string Name { get; set; }
public string Id { get; set; }
public string Matches { get; set; }
}
private ObservableCollection<Team> _teams=new ObservableCollection<Team>()
{
new Team()
{
Id="1",
Matches = "45",
Name = "TeamA"
},
new Team()
{
Id="1",
Matches = "45",
Name = "TeamB"
},
new Team()
{
Id="1",
Matches = "45",
Name = "TeamC"
}
};
public ObservableCollection<Team> Teams
{
get
{
return _teams;
}
set
{
if (_teams == value)
{
return;
}
_teams = value;
OnPropertyChanged();
}
}

WPF Combobox w/ Observable Collection in Composite Collection

I'm trying to populate a combobox with an observable collection and a header row using a composite collection. The header is there but I can't get the OC objects to populate. I'm just starting to use OC's so might be something basic I'm missing.
Customer class:
public class Customer : ViewModelBaseMain
{
public string CustomerName { get; set; }
public int CustomerId { get; set; }
}
I populate the OC, which is a dependency property, from a datatable (I verified that the OC is being populated correctly):
ObservableCollection<Customer> dpCustomerListOC = new ObservableCollection<Customer>();
foreach (DataRow drow in dpHeaderCustTable.Rows)
{
Customer Customer = new Customer();
Customer.CustomerId = Convert.ToInt32(drow["customer_id"]);
Customer.CustomerName = drow["customer_name"].ToString();
dpCustomerListOC.Add(Customer);
}
In the usercontrol resources, I specify the VM as a static resource:
<local:ReservationViewModel x:Key="ReserveVM"/>
I've tried two different ways from posts I've read.
First way w/ collection in the resources using x:reference:
<ComboBox x:Name="cboCustHeader" Grid.IsSharedSizeScope="True" IsEditable="False"
ItemsSource="{DynamicResource items}">
<ComboBox.Resources>
<CompositeCollection x:Key="items">
<ComboBoxItem IsEnabled="False">
<Border Style="{StaticResource ComboHeaderBorder}">
<Grid Style="{StaticResource ComboHeaderStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition Width="7"/>
<ColumnDefinition SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Grid.Children>
<TextBlock Grid.Column="0" Text="ID"/>
<TextBlock Grid.Column="2" Text="Name"/>
</Grid.Children>
</Grid>
</Border>
</ComboBoxItem>
<CollectionContainer Collection="{Binding Source={x:Reference cboCustHeader}, Path=DataContext.dpCustomerListOC}"/>
</CompositeCollection>
</ComboBox.Resources>
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition Width="7"/>
<ColumnDefinition SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Grid.Children>
<TextBlock Text="{Binding Path=CustomerId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="{Binding Path=CustomerName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid.Children>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Second way specifying the source:
<ComboBox x:Name="cboCustHeader" Grid.IsSharedSizeScope="True">
<ComboBox.ItemsSource>
<CompositeCollection>
<ComboBoxItem IsEnabled="False">
<Border Style="{StaticResource ComboHeaderBorder}">
<Grid Style="{StaticResource ComboHeaderStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition Width="7"/>
<ColumnDefinition SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Grid.Children>
<TextBlock Grid.Column="0" Text="ID"/>
<TextBlock Grid.Column="2" Text="Name"/>
</Grid.Children>
</Grid>
</Border>
</ComboBoxItem>
<CollectionContainer Collection="{Binding Path=dpCustomerListOC, Source={StaticResource ReserveVM}}"/>
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition Width="7"/>
<ColumnDefinition SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Grid.Children>
<TextBlock Grid.Column="0" Text="{Binding Path=CustomerId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Column="2" Text="{Binding Path=CustomerName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid.Children>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
Add a public property exposing the collection on your view model:
public ObservableCollection<Customer> DpCustomerListOC
{
get { return dpCustomerListOC; }
}
And bind the combo's ItemsSource to it:
<ComboBox x:Name="cboCustHeader" Grid.IsSharedSizeScope="True">
<ComboBox.ItemsSource>
<CompositeCollection>
<ComboBoxItem IsEnabled="False">
<Border Style="{StaticResource ComboHeaderBorder}">
<Grid Style="{StaticResource ComboHeaderStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition Width="7"/>
<ColumnDefinition SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Grid.Children>
<TextBlock Grid.Column="0" Text="ID"/>
<TextBlock Grid.Column="2" Text="Name"/>
</Grid.Children>
</Grid>
</Border>
</ComboBoxItem>
<CollectionContainer Collection="{Binding Path=DpCustomerListOC, Source={StaticResource ReserveVM}}"/>
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition Width="7"/>
<ColumnDefinition SharedSizeGroup="B"/>
</Grid.ColumnDefinitions>
<Grid.Children>
<TextBlock Grid.Column="0" Text="{Binding Path=CustomerId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Column="2" Text="{Binding Path=CustomerName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid.Children>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Make sure the collection is populated in the constructor.
It seems like you are missing to NotifyPropertyChange. When working with ComboBoxs I usually just do something like this:
<ComboBox ItemsSource="{Binding Customers, Mode=OneWay}"
SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}"> </ComboBox>
Then in your class:
public type Customers
{
get{return _Customers}
set{_SelectedCustomer = value;
NotifyPropertyChanged("Customers");}
}
public type SelectedCustomer
{
get{return _SelectedCustomer;}
set{_SelectedCustomer = value;
NotifyPropertyChanged("SelectedCustomer")}
}
The syntax may be off a little bit but something like this should populate the ComboBox.

How to Populate a Dual Column ListBox

I have a ListBox which has two columns. On a button click event I am adding the column data to an ObservableCollection, and I need to bind the collection to the ListBox to display the ObservableCollection data. I am having trouble figuring out how to do this.
MainPage.xaml
<ListBox x:Name="HistoryListBox" ItemsSource="{Binding Items}" Grid.Row="1" Grid.ColumnSpan="2">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ConnectionType}"/>
<TextBlock Grid.Column="1" Text="{Binding DateTime}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
MainPage.xaml.cs
public ObservableCollection<History> Items { get; set; }
public MainPage()
{
InitializeComponent();
Items = new ObservableCollection<History>();
HistoryListBox.DataContext = Items;
}
private void TestButton_Click(object sender, RoutedEventArgs e)
{
...
PopulateHistoryListBox();
}
private void PopulateHistoryListBox()
{
Items.Add(new History { ConnectionType = ConnectionTypeResultTextBlock.Text, DateTime = DateTime.Now });
}
I am not sure how to properly bind the ObservableCollection items to the ListBox? Also, I somehow need to persist this data historically for when the application reloads. How can I do this with IsolatedStorage?
You should assign ItemsSource ,
HistoryListBox.ItemsSource = Items;
If you are using MVVM,
Assign the ViewModel to the page, not to the listBox,
this.DataContext = ViewModel;
<ListBox x:Name="HistoryListBox" ItemsSource="{Binding Items}" Grid.Row="1" Grid.ColumnSpan="2">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Network}"/>
<TextBlock Grid.Column="1" Text="{Binding Date}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ListBox ItemsSource="{Binding Items, Mode=TwoWay}">
...
</ListBox>

Resize controls in a list to fill the available space WPF

I'm trying to find a way to display a horizontal List of items in WPF, the trick is that the Window which contains the List will be displayed on various screen sizes and all the items in the list need to be resized to fill the available space without any use of scroll bars.
I've found that a ViewBox control can be used to achieve the desired affect, but the ViewBox works only if I set <RowDefinition Height="300"/>.This approach doesn't work because if you have a certain number of items in the List they start becoming cut off.
If I remove <RowDefinition Height="300"/> then the first item in the list fills the screen and the rest are cut off
Any suggestions on how I can make all the items in the list resize to fill the available space no matter what the screen resolution is?
XAML:
<Window x:Class="ViewBoxExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" WindowState="Maximized">
<ItemsControl ItemsSource="{Binding Path=Employees}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<Viewbox Grid.Row="0" Grid.Column="0">
<TextBlock Text="{Binding Path=Name}" />
</Viewbox>
<Viewbox Grid.Row="0" Grid.Column="1">
<TextBlock Text="{Binding Path=Surname}" />
</Viewbox>
<Viewbox Grid.Row="0" Grid.Column="2">
<TextBlock Text="{Binding Path=Age}" />
</Viewbox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
C#:
using System.Collections.Generic;
using System.Windows;
namespace ViewBoxExample
{
public partial class MainWindow : Window
{
public List<Employee> _employees = new List<Employee>();
public List<Employee> Employees
{
get { return _employees; }
set { _employees = value; }
}
public MainWindow()
{
InitializeComponent();
Employees = new List<Employee>()
{
new Employee{ Name="Name1",Surname="Surname1",Age=20},
new Employee{ Name="Name2",Surname="Surname2",Age=30},
new Employee{ Name="Name3",Surname="Surname3",Age=40},
new Employee{ Name="Name4",Surname="Surname4",Age=50},
new Employee{ Name="Name5",Surname="Surname5",Age=60},
};
this.DataContext = this;
}
}
public class Employee
{
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
}
}
Just put your ItemsControl in ViewBox
<Viewbox>
<ItemsControl ItemsSource="{Binding Path=Employees}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<Viewbox Grid.Row="0" Grid.Column="0">
<TextBlock Text="{Binding Path=Name}" />
</Viewbox>
<Viewbox Grid.Row="0" Grid.Column="1">
<TextBlock Text="{Binding Path=Surname}" />
</Viewbox>
<Viewbox Grid.Row="0" Grid.Column="2">
<TextBlock Text="{Binding Path=Age}" />
</Viewbox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
EDIT: if i am doing the same type of thing i would use the actual Height and Width approach as explained by Kent Boogaart in this Answer

Categories

Resources