In a WPF project I have a ComboBox where the DataTemplate used for the ItemTemplate changes the Background colour of a Border based on the IsSelected property of the Person object that the ComboBoxItem's are bound to. So, in my example below, when IsSelected=true the Background=LightGreen.
All this is fine when the ComboBox's dropdown is open. However, when the dropdown closes after selecting an item with Background=LightGreen, the ComboBox's header doesn't show the LightGreen colour.
What do I need to do to show the LightGreen colour once the ComboBox has closed up on an IsSelected=true item?
Here is some example code to show what I mean.
XAML:
<Window x:Class="combo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:combo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding .}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Border HorizontalAlignment="Stretch">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}, Path=DataContext.IsSelected}" Value="True">
<Setter Property="Background" Value="LightGreen"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Email}">
</TextBlock>
</StackPanel>
</Border>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new Person[]
{
new Person() { Name = "Mickey" , Email= "m#disney.com" , IsSelected = false},
new Person() { Name = "Donald" , Email= "d#disney.com", IsSelected = true },
new Person() { Name = "Pluto" , Email= "p#disney.com", IsSelected = false }
};
}
}
public class Person
{
public string Name { get; set; }
public string Email { get; set; }
public bool IsSelected { get; set; }
}
The RelativeSource in your trigger looks for a ComboBoxItem, which you’ll only find in the ItemsPresenter inside the pop up the ComboBox.
When the pop up is closed, what we see is a ToggleButton and a ContentPresenter.
In case the marking didn’t give it away:
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=Content.IsSelected, RelativeSource={RelativeSource
AncestorType=ContentPresenter}}" Value="True">
<Setter Property="Background" Value="LightGreen"/>
</DataTrigger>
</Style.Triggers>
</Style>
I'm unsure about if you're talking about the way the ComboBox looks when closed, or if you are talking about the dropdown items background color not updating.
This answer already provides a solution for the first, but if you're wondering why the background color isn't updating on the selected item, it's because you are not binding ComboBoxItem.IsSelected to Person.IsSelected anywhere, so they are not synchronized.
Here's an example of how do add that binding :
<ComboBox.Resources>
<Style TargetType="{x:Type ComboBoxItem}" >
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ComboBox.Resources>
That said, you may still have issues because you are not setting the default selected item. Typically when wanting to provide for single selection capabilitilikethis, I see this done more in the style of
ObservableCollection<Person> People { get; set; }
Person SelectedPerson { get; set; }
with XAML being
<ComboBox ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson}" />
This way, you don't need to write any sync code to bind ComboBoxItem.IsSelected to your Person.IsSelected.
Related
for my work I need to generate datagrids via code behind. The datagrids contain 2 columns: Questions (Just text, which should not be editable) and "Answers" which needs a textBox or comboBox based on an enum.
I tried to bind a list of questions (Containing the text and an answer field) to the datagrid. The question column works just fine. But the textboxes don't receive any value. I can type in them, but once I sort the datagrid all values are gone. In the itemsource of the data grid I can see that the values doesn't update at all :/
After this failure I tried this with normal datagrid in XAML but it doesn't work either. The column for answers is a DataGridTemplate Column.
Here is my xaml:
<Window x:Class="BindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BindingTest"
xmlns:t="clr-namespace:BindingTest;assembly=BindingTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate x:Key="AnswerTemp">
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Typ}" Value="{x:Static t:QuestionType.Numeric}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Text="{Binding Path=Answer,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<DataGrid x:Name="MetaDataGrid" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Questions" Binding="{Binding Text}">
</DataGridTextColumn>
<DataGridTemplateColumn Header="Answers" CellTemplate="{StaticResource ResourceKey=AnswerTemp}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Code behind:
// Just contains a Dictionary <string,QuestionBlock> called SeperatorList
public static ActualPage actualPage = new ActualPage();
public MainWindow()
{
InitializeComponent();
QuestionBlock seperator = new QuestionBlock();
seperator.Questions = new ObservableCollection<Question>();
for (int counter = 1; counter < 11; counter++)
seperator.Questions.Add(new Question() { Text = $"What is the {counter}. Answer?", Answer = $"{5 + counter}", Typ = QuestionType.Numeric, Names = new List<string>() { "1", "2", "3" } });
actualPage.SeperatorList.Add("Test", seperator);
MetaDataGrid.ItemsSource = seperator.Questions;
}
Code in QuestionBlock:
public class QuestionBlock : INotifyPropertyChanged
{
private ObservableCollection<Question> _questionBlock;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Question> Questions
{
get => _questionBlock;
set
{
_questionBlock = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Questions)));
}
}
}
Code in Question:
public class Question : INotifyPropertyChanged
{
private string _text;
private string _answer;
private QuestionType _typ;
public event PropertyChangedEventHandler PropertyChanged;
public string Text
{
get => _text;
set
{
_text = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
}
}
public string Answer
{
get => _answer;
set
{
_answer = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Answer)));
}
}
public QuestionType Typ
{
get => _typ;
set
{
_typ = value;
}
}
}
What am I doing wrong here? Note: The values that should be included in the comboBox are not enum values! There are values from a string list. Any help would be appreciated :)
You should put the editable control in the CellEditingTemplate of the column:
<DataGridTemplateColumn Header="Answers" CellEditingTemplate="{StaticResource AnswerTemp}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Answer}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
...and bind the TextBox to the Answer property of the DataContext of the ContentControl:
<DataTemplate x:Key="AnswerTemp">
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Typ}" Value="{x:Static t:QuestionType.Numeric}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Text="{Binding Path=DataContext.Answer,
RelativeSource={RelativeSource AncestorType=ContentControl}}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
Basically I have a View which is bound to a ViewModel which has MenuItems.
What I want to do is that whenever the menu title is "-" I want to place a separator.
Theoretically it seems that I can avoid TemplateSelectors but if you think that it's inevitable please share even those solutions.
Here is the XAML:
<Window x:Class="WpfApp1.MenuItemStyle"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MenuItemStyle" Height="300" Width="300">
<DockPanel>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding MenuItems}">
<Menu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding Title}" Value="-">
<Setter Property="ContentTemplate">
<Setter.Value>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Separator />
</HierarchicalDataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
<TextBlock Text="{Binding Title}" Background="Red" />
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
<Grid>
</Grid>
</DockPanel>
</Window>
And here is the code behind:
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MenuItemStyle.xaml
/// </summary>
public partial class MenuItemStyle : Window
{
public MenuItemStyle()
{
InitializeComponent();
this.DataContext = this;
}
public ObservableCollection<MenuItem> MenuItems { get; set; } = new ObservableCollection<MenuItem> {
new MenuItem{ Title = "M1"
,Children= new ObservableCollection<MenuItem>{
new MenuItem{ Title = "M2"},
new MenuItem{ Title = "-"},
new MenuItem{ Title = "M3"},
}
}
};
}
public class MenuItem
{
public string Title { get; set; }
public ObservableCollection<MenuItem> Children { get; set; }
}
}
I have searched everywhere for a solution but couldn't find a pragmatic one.
You can create a Style for the MenuItems. Make it either local for a concrete menu instance (by placing in the menu's Resources) or place it in your resource dictionary:
<Menu ItemsSource="{Binding MenuItems}">
<Menu.Resources>
<Style TargetType="MenuItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Title}" Value="-">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Separator/>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Menu.Resources>
<Menu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Title}" Background="Red" />
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
When the visibility of a CarViewControl is set to collapsed, it still shows a placeholder where it used to be (see screenshot below).
Is there any way to completely hide a ListViewItem when it is Collapsed?
XAML Code
<ScrollViewer>
<ListView ItemsSource="{Binding CarVM.UserCars}" ShowsScrollingPlaceholders="False">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<ctrl:CarViewControl Car="{Binding}" Visibility="{Binding HideCar, Converter={ThemeResource InverseVisConverter}}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ScrollViewer>
In the image above, there are three CarViewControls which are collapsed, followed by one which is not. One is highlighted. I want them to be completely invisible when the content is collapsed.
What I've tried:
Setting height of the DataTemplate control to 0 (just to see if it hides the placeholder which had no effect
Setting ShowsScrollingPlaceholders to False based on this documentation: MSDN ListView Placeholders
Reason For Collapse Requirement
Within each CarViewControl, a WebView exists which includes a security token (which maintains that the WebView is logged into a specific web site). If you try to pass the WebView by reference, due to what I can only assume are security measures, you lose that security token and must re-login to the site. That is why adding/removing the control from the ObservableCollection will not work in my case.
I would say your design is flawed, but I can't fix that; so I'll provide a "workaround."
The issue is that your DataTemplate is collapsing, which is great, but clearly the container it is in doesn't collapse. This won't happen inherently because the parent won't inherit from a child. First realization is every item is wrapped in a ListViewItem and you can observe that from setting your ItemContainerStyle. This leaves you with two solutions (workarounds). You can either set up some triggers on your ListViewItem or you can do something easier like I did--and if you don't mind the UI affects.
My full working application is below. The main point is that you have to edit the layout/behavior of the ListViewItem. In my example, the default values aren't BorderThickeness and Padding isn't "0, 0, 0, 0"... Setting those to 0 will get hide your items completely.
MainWindow.xaml
<Window x:Class="CollapsingListViewItemContainers.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CollapsingListViewItemContainers"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"></BooleanToVisibilityConverter>
</Window.Resources>
<Grid>
<StackPanel>
<Button Content="Disappear Car 3" Click="Button_Click" />
<ListView ItemsSource="{Binding Cars}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="0 0 0 0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:Car}">
<Grid Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisConverter}}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Title}" />
<TextBlock Text="{Binding Id}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace CollapsingListViewItemContainers
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<Car> _cars = new ObservableCollection<Car>
{
new Car("Not yours", "Mine", 1),
new Car("Not mine", "Yours", 2),
new Car("Not ours", "His", 3),
new Car("Not ours", "Hers", 4),
};
public ObservableCollection<Car> Cars
{
get
{
return _cars;
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Cars[2].IsVisible = !Cars[2].IsVisible;
}
}
public class Car : INotifyPropertyChanged
{
private bool _isVisible;
public bool IsVisible
{
get
{
return _isVisible;
}
set
{
_isVisible = value;
NotifyPropertyChanged("IsVisible");
}
}
public string Name
{
get; set;
}
public string Title
{
get; set;
}
public int Id
{
get; set;
}
public Car(string name, string title, int id)
{
Name = name;
Title = title;
Id = id;
IsVisible = true;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Edit
Let's be honest, the above was a pretty cheap solution and I wasn't satisfied with it after thinking about it for another 3 minutes. The reason I'm dissatisfied is because you can still select the item if you have access to a keyboard. In the example above, click the first item, "hide" the item(s), then use your mouse and the ListView.SelectedItem will still change.
So below is a quick solution (workaround :D ) to actually remove the item from the list and preventing them from getting focus. Replace the ListView.ItemContainerStyle with this one and change the ActualHeight trigger value to match the values you're seeing. This will change based on OS themes I believe--I'll leave it up to you to test. Lastly, remember the ListViewItem.DataContext is going to be that of an item in the ItemsSource. This means the DataTrigger bound to IsVisible is bound to the Car.IsVisible property.
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Style.Triggers>
<Trigger Property="ActualHeight" Value="4">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
<DataTrigger Binding="{Binding IsVisible}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
Edit 2 (edited before I even posted the first edit.
Screw it, don't bind visibility of your CarViewControl; You don't need to. You solely need to focus on removing the item itself, and once the item is removed, the containing controls will be removed as well (though you should test this yourself and change IsTabStop and IsFocusable if you can still tab to items in CarViewControl). Also, since using an arbitrary number with the ActualHeight binding isn't very safe, just binding straight to the IsVisible property (or HideCar in your case) and triggering visibility of the ListViewItem should be sufficient.
Finally, here is my final XAML:
<Window x:Class="CollapsingListViewItemContainers.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CollapsingListViewItemContainers"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"></BooleanToVisibilityConverter>
</Window.Resources>
<Grid>
<StackPanel>
<Button Content="Disappear Car 3" Click="Button_Click" />
<ListView ItemsSource="{Binding Cars}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding IsVisible}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:Car}">
<Grid>
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Title}" />
<TextBlock Text="{Binding Id}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Window>
Instead of attempting to hide item via the UI, I would remove the item from the View or Collection that the ItemsSource is bound against. It's a cleaner approach, and the UI never has to be concerned about Visibility of an item.
EDIT 1
When a user selects a specific car from the quick select menu, it shows that car and hides the others.
Makes sense; let's call that view "UserCars", and assume the ItemsSource is bound to UserCars.
How about this: change the ItemsSource to be bound to "SelectedCollection". When you want to show UserCars, point SelectedCollection to the UserCars collection.
To limit the set, you'd simply point SelectedCollection to a new SingleCar that you populate with only UserCars.SelectedItem
XAML:
<ListView ItemsSource="{Binding SelectedCollection}" SelectedItem="{Binding SelectedCar}">
ViewModel:
private Car _selectedCar;
public Car SelectedCar {
get { return _selectedCar; }
set { _selectedCar = value; OnPropertyChanged("SelectedCar"); }
}
private ObservableCollection<Car> _selectedCollection = CarVM.UserCars;
private ObservableCollection<Car> SelectedCollection {
get { return _selectedCollection; }
set { _selectedCollection = value; OnPropertyChanged("SelectedCollection"); }
}
private ObservableCollection<Car> _originalCollectionForReferenceKeepingOnly;
// called via RelayCommand
public void UserJustSelectedACar()
{
ObservableCollection<Car> singleItemCollection = new ObservableCollection<Car>();
singleItemCollection.Add(SelectedCar);
_originalCollectionForReferenceKeepingOnly = SelectedCollection;
SelectedCollection = singleItemCollection;
}
public void ReturnToFullUsedCarsList()
{
SelectedCollection = _originalCollectionForReferenceKeepingOnly;
}
EDIT 2
It appears that you are trying to make a ListView pretend to be a "Car Details" view by hiding these other items. This is inherently a bad idea; a different UI element bound to the Listview's Selected Car Item would make for a better solution. Since the new detail panel would just be looking at an already-generated Car instance, you wouldn't incur any data hit. Even if you can make this approach work right now, I'm worried you're going to just cause yourself more grief in the future.
So I have a listbox and a tool bar in my WPF app. The tool bar just has regular controls, and the listbox has vertical expanders.
I need the listbox to have a different set of expanders depending on what button is clicked. Right now it looks like such:
<ListBox>
<local:Select_Analysis_Panel/>
</ListBox>
Where local:Select_Analysis_Panel is seperate user control file containing the expanders. What is the best way to go about dynamically updating the ListBox control's content upon a button click?
For the last couple hours I've been trying to use set DataTemplates for each expander set and bind the to the items control property with little avail with the code below. I'm just trying to get basic framework laid out before setting up a MVVM interface. Later on I was going to replace the ItemsSource="Network_anal" with you know ItemsSource="{Binding WhatExpanderViewModelProperty}" or something like that.
<ListBox Width="250" Margin="5,0,0,0">
<ListBox.Resources>
<DataTemplate DataType="Select_Analysis_Panel">
<local:Select_Analysis_Panel/>
</DataTemplate>
<DataTemplate x:Key="Network_anal" DataType="NetworkAnalysis">
<local:NetworkAnalysis/>
</DataTemplate>.Resources>
<ListBox.Template>
<ControlTemplate>
<Border Background="Red"/>
</ControlTemplate>
</ListBox.Template>
<ItemsControl ItemsSource="Network_anal"/>
</ListBox>
Am I taking the right approach to this at all?
Here's what I'm trying to do. Below when the "File" button is clicked the side bar displays these 2 expanders:
And when "Network Design" button these expanders are dipslayed:
Option 1:
Subclassing the sections:
each of these sections could be subclassed from a base section class and a specific DataTemplate could be used for each:
<Window x:Class="MiscSamples.MultiToolbar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MiscSamples"
Title="MultiToolbar" Height="300" Width="300">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<DockPanel>
<ListBox ItemsSource="{Binding Sections}"
SelectedItem="{Binding SelectedSection}"
DisplayMemberPath="Name"
DockPanel.Dock="Top">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Setter Property="MinWidth" Value="80"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border BorderBrush="Black" BorderThickness="1">
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}">
<ContentPresenter ContentSource="Content"/>
</ToggleButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<ScrollViewer Width="300" DockPanel.Dock="Left">
<ContentPresenter Content="{Binding SelectedSection}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:FileSection}">
<TextBlock Text="User Control For File Section"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:NetworkDesignSection}">
<TextBlock Text="User Control For Network Design"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SelectAnalysisSection}">
<TextBlock Text="User Control For Select Analysis"/>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</ScrollViewer>
<Grid Background="Gray">
<TextBlock Text="Design Surface" TextAlignment="Center" VerticalAlignment="Center" FontWeight="Bold"/>
</Grid>
</DockPanel>
</Window>
Code Behind:
public partial class MultiToolbar : Window
{
public MultiToolbar()
{
InitializeComponent();
var vm = new MainViewModel();
vm.Sections.Add(new FileSection() {Name = "File"});
vm.Sections.Add(new NetworkDesignSection() { Name = "Network Design" });
vm.Sections.Add(new SelectAnalysisSection() { Name = "Select Analysis" });
DataContext = vm;
}
}
Main ViewModel:
public class MainViewModel: PropertyChangedBase
{
private ObservableCollection<Section> _sections;
public ObservableCollection<Section> Sections
{
get { return _sections ?? (_sections = new ObservableCollection<Section>()); }
}
private Section _selectedSection;
public Section SelectedSection
{
get { return _selectedSection; }
set
{
_selectedSection = value;
OnPropertyChanged("SelectedSection");
}
}
}
Sections:
public abstract class Section:PropertyChangedBase
{
public string Name { get; set; }
private bool _isEnabled = true;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
private bool _isVisible = true;
public bool IsVisible
{
get { return _isVisible; }
set
{
_isVisible = value;
OnPropertyChanged("IsVisible");
}
}
//Optionally
//public string ImageSource {get;set;}
//ImageSource = "/Resources/MySection.png";
}
public class FileSection: Section
{
///... Custom logic specific to this Section
}
public class NetworkDesignSection:Section
{
///... Custom logic specific to this Section
}
public class SelectAnalysisSection: Section
{
///... Custom logic specific to File Section
}
//...etc etc etc
Result:
Notice that I'm using ToggleButtons bound to the ListBoxItem.IsSelected property to simulate a TabControl-like behavior.
You can set the DataContext of the whole form and bind the ItemsSource of the listbox, or set ItemsSource of the listbox to some collection directly.
I'd like to be able to change the DataTemplate that my custom class is using, based on a property in the ViewModel.
I can't find any clear examples and I feel like I might not know enough about WPF or XAML to know if this is even possible.
My ViewModel property represents whether the user has collapsed a column on one side of the application. If the column is collapsed, I want to only show the Image for each user, and if the column is expanded, I'll show the picture, first name and last name in a StackPanel.
I feel like there is something really basic that I just don't understand yet and I guess I'm looking for someone who maybe tried something like this or knows how to do this the right way.
User.cs
public class User
{
public string ImageFile {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
}
I'm using an ObservableCollection<User> to hold my collection of User objects in the viewmodel.
My 2 DataTemplates that I'd like to use. (right now I'm just using a default image and text to see how it looks)
DataTemplates
<DataTemplate x:Key="UserCollapsed">
<Image Source="/Images/anon.png"
Height="50"
Width="50"
Margin="0,5,0,0"/>
</DataTemplate>
<DataTemplate x:Key="UserExpanded">
<StackPanel>
<Image Source="/Images/anon.png"
Height="50"
Width="50"
Margin="0,5,0,0"/>
<TextBlock Text="Firstname"/>
<TextBlock Text="Lastnamehere"/>
</StackPanel>
</DataTemplate>
I've tried to write a style, and apply that to my ItemsControl in the view, and I've tried writing a datatemplate that uses triggers to decide which template to use, but I can't quite figure out where I'm going wrong.
Style
<Style x:Key="userTemplateStyle" TargetType="ItemsControl">
<Setter Property="ItemTemplate" Value="{StaticResource UserExpanded}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel}}" Value="True">
<Setter Property="ItemTemplate" Value="{StaticResource UserCollapsed}"/>
</DataTrigger>
</Style.Triggers>
</Style>
I get the following exception when I add the Style property on my ItemsControl in XAML.
Exception
{"Unable to cast object of type 'MS.Internal.NamedObject' to type 'System.Windows.DataTemplate'."}
And the DataTemplate that I tried to use as the ItemTemplate of the ItemsControl. (I feel like this is the wrong way to go about it but I tried anyway)
DataTemplate
<DataTemplate DataType="{x:Type md:CUser}">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel}}" Value="True">
<Setter Property="DataTemplate" Value="{StaticResource UserCollapsed}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
ItemsControl
<ItemsControl Visibility="{Binding ColumnVisibility}"
Style="{StaticResource userTemplateStyle}"
BorderThickness="0"
Name="itcLoggedInUsers"
Margin="0"
ItemsSource="{Binding LoggedInUsers}"
Grid.Row="1"/>
Your code works fine for me...
<Window x:Class="WpfApplication8.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfApplication8="clr-namespace:WpfApplication8"
Title="MainWindow"
Width="525"
Height="350">
<Window.Resources>
<WpfApplication8:ViewModel x:Key="ViewModel" />
<DataTemplate x:Key="UserCollapsed" />
<DataTemplate x:Key="UserExpanded">
<StackPanel Width="200"
Height="200"
Background="Red">
<TextBlock Text="Firstname" />
<TextBlock Text="Lastnamehere" />
</StackPanel>
</DataTemplate>
<Style x:Key="userTemplateStyle" TargetType="ItemsControl">
<Setter Property="ItemTemplate" Value="{StaticResource UserExpanded}" />
<Style.Triggers>
<DataTrigger Binding="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel}}" Value="True">
<Setter Property="ItemTemplate" Value="{StaticResource UserCollapsed}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid x:Name="grid" DataContext="{StaticResource ViewModel}">
<ItemsControl Name="itcLoggedInUsers"
Grid.Row="1"
Margin="0"
BorderThickness="0"
ItemsSource="{Binding LoggedInUsers}"
Style="{StaticResource userTemplateStyle}" />
</Grid>
And the ViewModel
public class ViewModel: INotifyPropertyChanged
{
private bool _columnIsCollapsed;
public ViewModel()
{
ColumnIsCollapsed = false;
LoggedInUsers = new ObservableCollection<User>();
LoggedInUsers.Add(new User(){FirstName = "SSSSSS", LastName = "XXXXXX"});
}
public bool ColumnIsCollapsed
{
get { return _columnIsCollapsed; }
set
{
_columnIsCollapsed = value;
OnPropertyChanged(new PropertyChangedEventArgs("ColumnIsCollapsed"));
}
}
public ObservableCollection<User> LoggedInUsers { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
public class User
{
public string ImageFile { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
I over thought this problem. If I needed to show or hide an element in the DataTemplate based on a boolean property of the ViewModel, I could just bind the visibility of the element to the property on the ViewModel, and use a converter to return a Visibility.
Solution
<DataTemplate DataType="{x:Type md:User}">
<StackPanel>
<Image Source="/Images/anon.png"
Height="50"
Width="50"
Margin="0,5,0,0"/>
<TextBlock Text="Firstname" Visibility="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel},Converter={StaticResource InvertBoolVisibility}}"/>
<TextBlock Text="Lastnamehere" Visibility="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel},Converter={StaticResource InvertBoolVisibility}}"/>
</StackPanel>
</DataTemplate>
Converter
public class InvertBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var theBool = (bool)value;
if (theBool)
return Visibility.Collapsed;
else
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}