WPF Binding a Listbox to thing that has sub enumerables - wpf

I have a class like this:
public class UIThing {
public string Name{get;set}
public IEnumerable<Thing> LotsOfThings;
}
I have some of these in a List (List<UIThings>) and I would like to bind them into a ListBox so that the LotsOfThings member is expanded as items in the ListBox. Like a list of lists I guess. But I can't get my head around the DataTemplate needed.
Any Ideas?

This may give you the idea to do it:
I recommend you to use ItemsControl
public class UIThing
{
public string Name { get; set; }
public List<string> LotsOfThings { get; set; }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
UIThing item1 = new UIThing() { Name = "A", LotsOfThings = new List<string>() { "1A", "2A", "3A", "4A" } };
UIThing item2 = new UIThing() { Name = "B", LotsOfThings = new List<string>() { "1B", "2B", "3B", "4B" } };
UIThing item3 = new UIThing() { Name = "C", LotsOfThings = new List<string>() { "1C", "2C", "3C", "4C" } };
UIThing item4 = new UIThing() { Name = "D", LotsOfThings = new List<string>() { "1D", "2D", "3D", "4D" } };
var list = new List<UIThing>() { item1, item2, item3, item4 };
itemsControl.ItemsSource = list;
}
And this is the XAML:
<ItemsControl Name="itemsControl" HorizontalAlignment="Left" Width="100">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Name}" Margin="0,5" Width="auto">
<ListBox Width="auto" ItemsSource="{Binding LotsOfThings}" Margin="20,0,0,0" Background="AliceBlue"></ListBox>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Result:
EDIT: here is the ListBox version
<ListBox Name="itemsControl" Margin="0,0,197,0">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Label Content="{Binding Name}" Margin="0,5"></Label>
<Border Margin="20,0,0,0" Background="AliceBlue" CornerRadius="10">
<ListBox Width="auto" ItemsSource="{Binding LotsOfThings}" ></ListBox>
</Border>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Can use a GridView can ComboBox
<GridViewColumn Width="100">
<GridViewColumnHeader Content="Main"/>
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=GroupsMain, Mode=OneWay}" DisplayMemberPath="Name" IsEditable="False" SelectedIndex="0" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
If you need everything in a single column then a TreeView as suggested byt Matt

You'll need a DataTemplate for your UIThing. In that template you will have another ListView that is bound to your LotsOfThings list. Then you can add a DataTemplate for your Thing class too.

Related

Bind ListViewItem ContextMenu MenuItem Command to ViewModel of the ListView's ItemsSource

I have a ListView of Expanders. Each Expander (representing a database table) will have items under it, in another ListView. I want to Right-Click and have an "Edit" option on the innermost items, which represent records in the corresponding database table.
There is an ICommand named 'Edit' in my MainEditorViewModel. The Datacontext in which this command resides is the same as that of the outermost ListView named "TestGroupsListView"
Here is the XAML markup for the ListView of Expanders. The outermost ListView I've named for referencing in the binding via ElementName for the MenuItem's Binding:
<ListView Name="TestGroupsListView" ItemsSource="{Binding TestGroups}" Grid.Row="1">
<ListView.ItemTemplate>
<DataTemplate>
<Expander Style="{StaticResource MaterialDesignExpander}" >
<Expander.Header>
<Grid MaxHeight="50">
<TextBlock Text="{Binding Name}"/>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Add..." Command="{Binding Add}"/>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Expander.Header>
<ListView ItemsSource="{Binding Records}" Style="{StaticResource MaterialDesignListView}" Margin="30 0 0 0">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit"
Command="{Binding ElementName=TestGroupsListView, Path=DataContext.Edit}"
CommandParameter="{Binding }"/>
</ContextMenu>
</Grid.ContextMenu>
<Button Content="{Binding RecordName}" Command="{Binding ElementName=TestGroupsListView, Path=DataContext.Edit}"/>
<!--<TextBlock Text="{Binding RecordName}" AllowDrop="True"/>-->
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I am able to bind a button in the DataTemplate to 'Edit' successfully, but when I attempt to bind the MenuItem's Command to 'Edit', nothing happens. Why might this be that the button command binding works using ElementName but the same binding in the ContextMenu doesn't?
I think it will be better to use the context menu globally for ListView and globally for each Child ListView. Ok, here is my solution:
<ListBox ItemsSource="{Binding Groups}">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Add..." Command="{Binding Add}"/>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Name}">
<ListView ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit" Command="{Binding Edit}" IsEnabled="{Binding CanEdit}"/>
</ContextMenu>
</ListView.ContextMenu>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And for better understanding code behind:
public class GroupsVM : ViewModelBase
{
public ICommand Add
{
get => null; //Command implementation
}
public ObservableCollection<GroupVM> Groups { get; set; } = new ObservableCollection<GroupVM>()
{
new GroupVM { Name = "First" },
new GroupVM { Name = "Second" },
new GroupVM { Name = "Third" }
};
}
public class GroupVM : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
public ICommand Edit
{
get => null; //Command implementation
}
public bool CanEdit => SelectedRecord != null;
public ObservableCollection<RecordVM> Records { get; set; } = new ObservableCollection<RecordVM>()
{
new RecordVM { Name="Record1" },
new RecordVM { Name="Record2" },
new RecordVM { Name="Record3" }
};
private RecordVM _selectedRecord = null;
public RecordVM SelectedRecord
{
get => _selectedRecord;
set
{
_selectedRecord = value;
OnPropertyChanged();
OnPropertyChanged("CanEdit");
}
}
}
public class RecordVM : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
}

Binding several ItemsControl to one ObservableCollection including visibility

In general, we can not bind multiple controls to one ObservableCollection
Is it possible to do this in the following situation?
Only one part is visible at a time
In this situation, there is a reference error twice to the same collection
How does it actually work internally? Should it not include invisible code?
<Grid Visibility="{Binding B1Visible, Converter={StaticResource BooleanToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding Elements, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!--Another code for B1-->
</Grid>
<Grid Visibility="{Binding B2Visible, Converter={StaticResource BooleanToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding Elements, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!--Another code for B2 -->
</Grid>
I don't know what Elements is, and I don't know where you're headed using a grid for the itemspanel of your itemscontrol.
But.
You can bind to the same observablecollection multiple times.
In the code below the two visibility properties are Boolean and the converter translates true into Visibility.Visible and false into Visibility.Collapsed.
public class MainWindowViewModel : BaseViewModel
{
private ObservableCollection<Person> people = new ObservableCollection<Person>();
public ObservableCollection<Person> People
{
get { return people; }
set { people = value; RaisePropertyChanged();}
}
private bool b1Visible = true;
public bool B1Visible
{
get { return b1Visible; }
set { b1Visible = value; RaisePropertyChanged(); }
}
private bool b2Visible = true;
public bool B2Visible
{
get { return b2Visible; }
set { b2Visible = value; RaisePropertyChanged(); }
}
public MainWindowViewModel()
{
People.Add(new Person { FirstName = "Chesney", LastName = "Brown" });
People.Add(new Person { FirstName = "Gary", LastName = "Windass" });
People.Add(new Person { FirstName = "Liz", LastName = "McDonald" });
People.Add(new Person { FirstName = "Carla", LastName = "Connor" });
}
}
My markup:
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<Grid Visibility="{Binding B1Visible, Converter={StaticResource BooleanToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding People}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding LastName}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--Another code for B1-->
</Grid>
<Grid Visibility="{Binding B2Visible, Converter={StaticResource BooleanToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding People}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding LastName}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</StackPanel>
Both itemscontrols are visible and so I see the list of names twice.

Bind to ListBox selectedItem inside TabControl

Okay, I got this Tabcontrol containing a ListBox. Now my problem it that I would like to bind <TextBox x:Name="DetailTextBox" Text="{Binding Detail}"/> to the selectedItem in the listbox and show the Detail property value.
Note that the TextBox is not part of the TabControl, but is in another Column.
I can't quite figure out, how to handle binding, when there a multiple ListBox'es, one in each TabControl Item.
My classes
public class TabViewModel
{
public string Name { get; set; }
public ObservableCollection<TabItemViewModel> Collection { get; set; }
}
public class TabItemViewModel
{
public string Title { get; set; }
public string Detail { get; set; }
}
public MainWindow()
var tabViewModels = new ObservableCollection<TabViewModel>();
tabViewModels.Add(new TabViewModel{Name = "Tab 1", Collection = new ObservableCollection<TabItemViewModel>{new TabItemViewModel{Detail = "Detail 1.1", Title = "Title 1.1"}, new TabItemViewModel{Detail = "Detail 2.2", Title = "Title 2.2"}}});
tabViewModels.Add(new TabViewModel { Name = "Tab 2", Collection = new ObservableCollection<TabItemViewModel> { new TabItemViewModel { Detail = "Detail 2.1", Title = "Title 2.1" }, new TabItemViewModel { Detail = "Detail 2.2", Title = "Title 2.2" } } });
tabViewModels.Add(new TabViewModel { Name = "Tab 3", Collection = new ObservableCollection<TabItemViewModel> { new TabItemViewModel { Detail = "Detail 3.1", Title = "Title 3.1" }, new TabItemViewModel { Detail = "Detail 3.2", Title = "Title 3.2" } } });
DataContext = tabViewModels;
MainWindow.xaml.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*"/>
<ColumnDefinition Width="50*"/>
</Grid.ColumnDefinitions>
<TabControl ItemsSource="{Binding}" Grid.Column="0" SelectedIndex="0">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header">
<Setter.Value>
<Binding Path="Name"/>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding Collection}">
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Title}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<StackPanel Grid.Column="1">
<TextBox x:Name="DetailTextBox" Text="{Binding Detail}"/>
</StackPanel>
</Grid>
EDIT
Temp Solution
Found a way to make it work, but I'm still looking for a pure Xaml solution.
Added a SelectionChange event
<ListBox ItemsSource="{Binding Collection}" SelectionChanged="ListBox_SelectionChanged">
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
DetailTextBox.DataContext = (TabItemViewModel) e.AddedItems[0];
}
How about this, I was surprised myself :-)
Make these changes to your Xaml.
<TabControl ItemsSource="{Binding}"
Grid.Column="0" SelectedIndex="0"
IsSynchronizedWithCurrentItem="True">
<ListBox ItemsSource="{Binding Collection}"
IsSynchronizedWithCurrentItem="True">
<TextBox x:Name="DetailTextBox"
Text="{Binding /Collection/Detail}"/>
The '/' binds to the currently selected item of a control's CollectionView.
So the binding above is drilling down through
The currently SelectedItem of the ObservableCollection set on the Data Context
The Collection property on that item
The currently SelectedItem of the Collection property (ObservableCollection)
The Detail property on that item.
In order for this to work we need to specify IsSynchronizedWithCurrentItem="True" to ensure the SelectedItem remains synchronized with the current item of each collection.

How can I track the selected TabPage's DataContext from my ViewModel?

I have TabItem class:
public class TabItem
{
public string Header { get; set; }
public IView Content { get; set; }
}
and in my model:
public ObservableCollection<TabItem> Tabs
{
get { return _tabs; }
set
{
if(_tabs!=value)
{
_tabs = value;
RaisePropertyChanged("Tabs");
}
}
}
public TabItem CurrentTabItem
{
get { return _currentTabItem; }
set
{
if (_currentTabItem != value)
{
}
_currentTabItem = value;
RaisePropertyChanged("CurrentTabItem");
}
}
In View i'm binding to ModelView:
<TabControl x:Name="shellTabControl" ItemsSource="{Binding Tabs}"
IsSynchronizedWithCurrentItem="True" SelectionChanged="ShellTabControlSelectionChanged">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Content}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
From view i want to change ViewModel's CurrentTabItem property:
private void ShellTabControlSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(e.Source is TabItem)
{
var tabItem = e.Source as TabItem;
ViewModel.CurrentTabItem = tabItem; //don't work
}
}
What is the best approach to convert TabControl's TabItem to my TabItem?
Maybe it is better to use SelectedItem="{Binding CurrentTabItem, Mode=TwoWay, UpdateSourceTrigget=PropertyChanged}"?
<TabControl x:Name="shellTabControl" ItemsSource="{Binding Tabs}"
IsSynchronizedWithCurrentItem="True"
SelectionChanged="ShellTabControlSelectionChanged"
SelectedItem={Binding Path=CurrentTabItem,Mode=Twoway}>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Content}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
This will get you the selected TabItem.......
Also change the Name of your Custom "TabItem" its confusing ;)

Silverlight the first combobox filter the second combobox

I have two comboboxes created as common controls. When the page is loaded, the programcbo lists the programs names; and the teamcbo list the team names. I want to ONLY display the related team names when any program name has been selected. In a word, I need to filter the second combobox by selecting the name from the first combobox.
Thanks in advance.
There are a variety of ways you could handle this.
If you have, for example, "Program" objects that contain collections of "Teams", you could do it like this, almost all in XAML:
<StackPanel>
<ComboBox x:Name="programCbo" ItemsSource="{Binding}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ProgramName}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox x:Name="teamCbo" ItemsSource="{Binding ElementName=programCbo, Path=SelectedItem.Teams}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TeamName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
You can see here that I've bound the first combobox to the datacontext, which is a list of Programs (we'll set that in the next bit). The second combobox is set to the selecteditem property of the first combobox, and then the Teams property on that. This way, when the selection changes on the first combobox, databinding kicks in and causes the itemssource on the second box to update.
In the code behind, I just build up the datasource. Obviously you'd have to get your data your own way:
public MainPage()
{
InitializeComponent();
DataContext = new List<Program>
{
new Program
{
ProgramName = "Program 1",
Teams = new List<Team>
{
new Team
{
TeamName = "Program 1 Team 1"
},
new Team
{
TeamName = "Program 1 Team 2"
}
}
},
new Program
{
ProgramName = "Program 2",
Teams = new List<Team>
{
new Team
{
TeamName = "Program 2 Team 1"
},
new Team
{
TeamName = "Program 2 Team 2"
}
}
}
};
}
If you don't have your data in a way that's accessible like this, you'll have to handle the Selection changed event on your combobox:
(XAML)
<StackPanel>
<ComboBox x:Name="programCbo" SelectionChanged="programCbo_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ProgramName}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox x:Name="teamCbo">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TeamName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
Notice this time, we've set a handler for the SelectionChanged event.
(CodeBehind)
public MainPage()
{
InitializeComponent();
programCbo.ItemsSource = new List<Program>
{
new Program
{
ProgramName = "Program 1",
},
new Program
{
ProgramName = "Program 2",
}
};
}
private void programCbo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// get the sender
ComboBox cb = sender as ComboBox;
// get the selected program
Program selectedProgram = (cb.SelectedItem as Program);
// do some stuff to get the appropriate teams and set the other combobox's itemssource to it
teamCbo.ItemsSource = new List<Team>
{
new Team
{
TeamName = "My favorite team!"
}
};
}
And there you have it. Long winded, but hopefully thorough with examples :)
You can use a paged collection to filter or you can modify your observable collection which is bound to the second combobox when the value that's bound to the first combobox raises on prop change.
xaml:
...
xmlns:local="clr-namespace:SelectedProgram">
<UserControl.Resources>
<local:MainPageViewModel x:Key="vm" />
</UserControl.Resources>
<Grid x:Name="LayoutRoot" DataContext="{Binding Source={StaticResource vm}}">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="350" />
</Grid.RowDefinitions>
<sdk:Label Content="Select Team for the Program" Grid.Row="0" Margin="20,0,0,0"/>
<StackPanel Orientation="Horizontal" Grid.Row="1" VerticalAlignment="Top" Margin="20,0,0,0">
<ComboBox x:Name="programcbo" Width="100" Height="25"
ItemsSource="{Binding Path=Programs, Mode=OneWay}"
SelectedValue="{Binding Path=SelectedTeam, Mode=TwoWay}"
DisplayMemberPath="Key"
SelectedValuePath="Value" />
<ComboBox x:Name="teamcbo" Width="100" Height="25" Margin="20"
ItemsSource="{Binding Path=SelectedTeam, Mode=OneWay}"/>
</StackPanel>
</Grid>
In class like MainPageViewModel:
...
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Windows.Data;
namespace SelectedProgram
{
public class MainPageViewModel : ModelBase
{
public MainPageViewModel()
{
this.Programs = GetPrograms();
}
private string[] _selectedTeam;
public string[] SelectedTeam
{
get { return _selectedTeam; }
set
{
_selectedTeam = value;
FirePropertyChanged("SelectedTeam");
}
}
private ObservableCollection<KeyValuePair<string, string[]>> _programs;
public ObservableCollection<KeyValuePair<string, string[]>> Programs
{
get { return _programs; }
set
{
_programs = value;
FirePropertyChanged("Programs");
}
}
private ObservableCollection<KeyValuePair<string, string[]>> GetPrograms()
{
ObservableCollection<KeyValuePair<string, string[]>> pr = new ObservableCollection<KeyValuePair<string, string[]>>();
pr.Add(new KeyValuePair<string, string[]>("Janitors", new string[]{ "Schwarzkopf", "Ivanov", "Smith"}));
pr.Add(new KeyValuePair<string, string[]>("Violonists", new string[] { "Einstein", "Odinzovobulyznicof", "Onestone" }));
pr.Add(new KeyValuePair<string, string[]>("Programers", new string[] { "Petrov", "Skeet", "Stroustrup" })); return pr;
}
}
}

Resources