I know this may be a duplicate but no solution works for me. So I have a class Technik which has these properties:
public class Technik
{
public bool checkedTe { get; set; }
public int TechnikID { get; set; }
public string anlagengruppe { get; set; }
public string techniktyp { get; set; }
public string anlage { get; set; }
public string bemerkung { get; set; }
}
Now I have a DataTable with 216 rows and each row is getting into a Technik object that is added into my ObservableCollection<Technik> like:
foreach (DataRow dr in dtTechnik.Rows)
{
Technik technik = new Technik();
technik.checkedTe = (bool)dr.ItemArray[0];
technik.TechnikID = (int)dr.ItemArray[1];
technik.anlagengruppe = (string)dr.ItemArray[2];
technik.techniktyp = (string)dr.ItemArray[3];
technik.anlage = (string)dr.ItemArray[4];
technik.bemerkung = (string)dr.ItemArray[5];
TechnikCollection.Add(technik);
}
I want to bind my ObservableCollection like:
* anlagengruppe
* techniktyp
*anlage
* TechnikID
Right now I'm getting nowhere, so maybe you guys out there can help me.
Actual my tree view looks like this:
<TreeView x:Name="treeView" HorizontalAlignment="Left" Height="850" Margin="10,0,0,0" VerticalAlignment="Top" Width="464"
ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Visible" ItemsSource="{Binding TechnicTable}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding TechnicTable}">
<TextBlock Text="{Binding Path=anlagengruppe}" />
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding techniktyp}" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Edit:
Maybe some of you think that my tree view ItemsSource is not the correct collection, this is the right one, there is some more code where I change collections.
I am skeptical of this design. Why do you feel it's useful and helpful to the user to present a single object and its property values as if that object had some hierarchical structure to it?
If all you're trying to do is impose some visual structure on the user interface, that's easily done without using TreeView. For example:
class TableItem
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
public TableItem() { }
public TableItem(string property1, string property2, string property3)
{
Property1 = property1;
Property2 = property2;
Property3 = property3;
}
}
class ViewModel
{
public ObservableCollection<TableItem> TableItems { get; } = new ObservableCollection<TableItem>();
}
<Window x:Class="TestSO46300831HiearchicalObservable.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:l="clr-namespace:TestSO46300831HiearchicalObservable"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:ViewModel>
<l:ViewModel.TableItems>
<l:TableItem Property1="Item #1, property #1"
Property2="Item #1, property #2"
Property3="Item #1, property #3"/>
<l:TableItem Property1="Item #2, property #1"
Property2="Item #2, property #2"
Property3="Item #2, property #3"/>
<l:TableItem Property1="Item #3, property #1"
Property2="Item #3, property #2"
Property3="Item #3, property #3"/>
</l:ViewModel.TableItems>
</l:ViewModel>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type l:TableItem}">
<StackPanel>
<TextBlock Text="{Binding Property1}"/>
<TextBlock Text="{Binding Property1}" Margin="10,0,0,0"/>
<TextBlock Text="{Binding Property1}" Margin="20,0,0,0"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ListBox ItemsSource="{Binding TableItems}"/>
</StackPanel>
</Window>
That said, if you must use TreeView and you want for the view to update as the collection is modified, it seems to me you can accomplish that by using an intermediate collection that implements INotifyCollectionChanged (easily done simply by inheriting ObservableCollection<T> and tracking the original collection. The intermediate collection is needed, so that items can be converted from the original single-object item to a hierarchical item type that can be used with the TreeView class. For example:
class HierarchicalTableItem
{
public string Text { get; }
public IReadOnlyList<HierarchicalTableItem> Items { get; }
public HierarchicalTableItem(string text, HierarchicalTableItem child = null)
{
Text = text;
Items = child != null ? new[] { child } : null;
}
}
class ViewModel
{
public ICommand AddCommand { get; }
public ICommand InsertCommand { get; }
public ICommand RemoveCommand { get; }
public int Index { get; set; }
public ObservableCollection<TableItem> TableItems { get; } = new ObservableCollection<TableItem>();
public ViewModel()
{
AddCommand = new DelegateCommand(() => TableItems.Add(_CreateTableItem()));
InsertCommand = new DelegateCommand(() => TableItems.Insert(Index, _CreateTableItem()));
RemoveCommand = new DelegateCommand(() => TableItems.RemoveAt(Index));
}
private int _itemNumber;
private TableItem _CreateTableItem()
{
_itemNumber = (_itemNumber < TableItems.Count ? TableItems.Count : _itemNumber) + 1;
return new TableItem(
$"Item #{_itemNumber}, property #1",
$"Item #{_itemNumber}, property #2",
$"Item #{_itemNumber}, property #3");
}
}
class ConvertingObservableCollection<T> : ObservableCollection<object>
{
private readonly IValueConverter _converter;
private readonly ObservableCollection<T> _collection;
public ConvertingObservableCollection(IValueConverter converter, ObservableCollection<T> collection)
{
_converter = converter;
_collection = collection;
_ResetItems();
_collection.CollectionChanged += _OnCollectionChanged;
}
private void _OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
_AddItems(e);
break;
case NotifyCollectionChangedAction.Move:
_RemoveItems(e);
_AddItems(e);
break;
case NotifyCollectionChangedAction.Remove:
_RemoveItems(e);
break;
case NotifyCollectionChangedAction.Replace:
_ReplaceItems(e);
break;
case NotifyCollectionChangedAction.Reset:
_ResetItems();
break;
}
}
private void _ReplaceItems(NotifyCollectionChangedEventArgs e)
{
for (int i = 0; i < e.NewItems.Count; i++)
{
this[i] = _Convert(e.NewItems[i]);
}
}
private void _AddItems(NotifyCollectionChangedEventArgs e)
{
for (int i = 0; i < e.NewItems.Count; i++)
{
Insert(i + e.NewStartingIndex, _Convert(e.NewItems[i]));
}
}
private void _RemoveItems(NotifyCollectionChangedEventArgs e)
{
for (int i = e.OldItems.Count - 1; i >= 0; i--)
{
RemoveAt(i + e.OldStartingIndex);
}
}
private void _ResetItems()
{
Clear();
foreach (T t in _collection)
{
Add(_Convert(t));
}
}
private object _Convert(object value)
{
return _converter.Convert(value, typeof(T), null, null);
}
}
class TableItemHierarchicalConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
TableItem tableItem = value as TableItem;
if (tableItem == null)
{
return Binding.DoNothing;
}
return new HierarchicalTableItem(tableItem.Property1,
new HierarchicalTableItem(tableItem.Property2,
new HierarchicalTableItem(tableItem.Property3)));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
class ConvertingCollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
IValueConverter converter = parameter as IValueConverter;
if (converter == null || value == null ||
value.GetType().GetGenericTypeDefinition() != typeof(ObservableCollection<>))
{
return Binding.DoNothing;
}
Type resultType = typeof(ConvertingObservableCollection<>).MakeGenericType(value.GetType().GenericTypeArguments);
return Activator.CreateInstance(resultType, converter, value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
<Window x:Class="TestSO46300831HiearchicalObservable.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:l="clr-namespace:TestSO46300831HiearchicalObservable"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:ViewModel>
<l:ViewModel.TableItems>
<l:TableItem Property1="Item #1, property #1"
Property2="Item #1, property #2"
Property3="Item #1, property #3"/>
<l:TableItem Property1="Item #2, property #1"
Property2="Item #2, property #2"
Property3="Item #2, property #3"/>
<l:TableItem Property1="Item #3, property #1"
Property2="Item #3, property #2"
Property3="Item #3, property #3"/>
</l:ViewModel.TableItems>
</l:ViewModel>
</Window.DataContext>
<Window.Resources>
<l:ConvertingCollectionConverter x:Key="convertingCollectionConverter1"/>
<l:TableItemHierarchicalConverter x:Key="tableItemConverter1"/>
</Window.Resources>
<ScrollViewer>
<StackPanel>
<UniformGrid Columns="4">
<Button Content="Add" Command="{Binding AddCommand}"/>
<Button Content="Insert" Command="{Binding InsertCommand}"/>
<Button Content="Remove" Command="{Binding RemoveCommand}"/>
<TextBox Text="{Binding Index}"/>
</UniformGrid>
<TreeView ItemsSource="{Binding TableItems,
Converter={StaticResource convertingCollectionConverter1},
ConverterParameter={StaticResource tableItemConverter1}}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Text}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
</ScrollViewer>
</Window>
This alternative relies on three key classes:
ConvertingObservableCollection<T> — This does the work of watching the original collection and presenting converted items according to the current state of that original collection.
ConvertingCollectionConverter — This converts the original collection to the ConvertingObservableCollection<T> object for the purpose of binding to the TreeView.
TableItemHierarchicalConverter — This converts the individual original item objects into a hierarchy of objects suitable for display in the TreeView.
Of course, there is also the simple container class HierarchicalTableItem which is used to represent the hierarchy for each table item.
Ultimately, the key to remember is that for a TreeView, you must be presenting items that have a recursive nature, such that a single HierarchicalDataTemplate element can be used to define how to present each level of the tree. This means that a single data item must have some property that can be used for the ItemsSource of the template, which is itself some type of collection of the same type of data item.
Related
I am trying to MultiBind the SelectedItems (ListView) and SelectedItem or SelectedCategory in my ViewModel (ComboBox) to a readonly TextBox. The OutputConverter just checks if there is at least one item selected (ListView) and TypeData (ComboBox) selected before creating the text for the TextBox.
However, if I try it only with that the TextBox only updates when the ComboBox.SelectedItem changes not when SelectedItems inside ListView changes.
So I also included from my ViewModel SelectedEntry (ListView) (which is same as SelectedItem) as a Binding for the MultiBinding.
Now I get the following:
Explanation:
The Selection is always one step behind and uses the previous SelectedItems from the ListView to bind to the TextBox.Text. Even when I am selecting multiple entries via CTRL or Shift + Click. However, if the ComboBox.SelectedItem changes it updates the TextBox as intended.
How do I get the behaviour if the Selection changes inside the ListView the TextBox immediately updates its content accordingly (preferably I would like to use the SelectedEntries of my ViewModel and not the SelectedItems of the ListView and if possible in a MVVM compliant way)?
Edit:
I noticed when the Converter is called the SelectedEntry (implements
INotifyPropertyChanged) is updated but the SelectedEntries
(ObservableCollection and implements INotifyPropertyChanged) is not.
I have also included a SelectionChanged event that calls a command
and passes the SelectedItems as a parameter to the command. If that
might help but I would rather like to have a proper binding that
updates accordingly.
Code:
Model TypeData (ComboBox):
public class TypeData : INotifyPropertyChanged
{
public enum Type
{
NotSet = '0',
A = 'A',
B = 'B',
C = 'C'
}
private string name;
public string Name
{
get { return name; }
set
{
name = value;
//OnPropertyChanged("Name");
OnPropertyChanged(nameof(Name));
}
}
private Type category;
public Type Category
{
get { return category; }
set { category = value; }
}
public TypeData(string name)
{
Name = name;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public override string ToString()
{
return Name;
}
}
Model Entry (ListView):
public class Entry : INotifyPropertyChanged
{
private string title;
public string Title
{
get { return title; }
set
{
title = value;
OnPropertyChanged(nameof(Title));
}
}
private string author;
public string Author
{
get { return author; }
set
{
author = value;
OnPropertyChanged(nameof(Author));
}
}
private string year;
public string Year
{
get { return year; }
set
{
year = value;
OnPropertyChanged(nameof(Year));
}
}
public Entry(string title, string author, string year)
{
Title = title;
Author = author;
Year = year;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ViewModel:
public class MainViewModel
{
public ObservableCollection<Entry> Entries { get; set; }
public Entry SelectedEntry { get; set; }
public ObservableCollection<Entry> SelectedEntries { get; set; }
public ObservableCollection<TypeData> Types { get; set; }
private TypeData selectedCategory;
public TypeData SelectedCategory { get; set; }
public RelayCommand<object> SelectionChangedCommand { get; set; }
public MainViewModel()
{
Entries = new ObservableCollection<Entry>
{
new Entry("Title1", "Author1", "Year1"),
new Entry("Title2", "Author2", "Year2"),
new Entry("Title3", "Author3", "Year3"),
new Entry("Title4", "Author4", "Year4"),
};
Types = new ObservableCollection<TypeData>
{
new TypeData("A"),
new TypeData("B"),
new TypeData("C"),
};
SelectionChangedCommand = new RelayCommand<object>(items =>
{
var selectedEntries = (items as ObservableCollection<object>).Cast<Entry>();
SelectedEntries = new ObservableCollection<Entry>(selectedEntries);
});
}
}
XAML:
<Window x:Class="MvvmMultiBinding.View.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:MvvmMultiBinding"
xmlns:m="clr-namespace:MvvmMultiBinding.Model"
xmlns:vm="clr-namespace:MvvmMultiBinding.ViewModel"
xmlns:conv="clr-namespace:MvvmMultiBinding.View.Converter"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:MainViewModel></vm:MainViewModel>
</Window.DataContext>
<Window.Resources>
<conv:OutputConverter x:Key="OutputConverter"/>
</Window.Resources>
<Grid>
<DockPanel>
<ListView Name="ListViewEntries" ItemsSource="{Binding Entries}" SelectedItem="{Binding SelectedEntry}" DockPanel.Dock="Top">
<ListView.View>
<GridView>
<GridViewColumn Header="Title" Width="250" DisplayMemberBinding="{Binding Title}" />
<GridViewColumn Header="Author" Width="150" DisplayMemberBinding="{Binding Author}" />
<GridViewColumn Header="Year" Width="50" DisplayMemberBinding="{Binding Year}" />
</GridView>
</ListView.View>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding DataContext.SelectionChangedCommand, ElementName=ListViewEntries}"
CommandParameter="{Binding SelectedItems, ElementName=ListViewEntries}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
<ComboBox ItemsSource="{Binding Types}" SelectedItem="{Binding SelectedCategory}" MinWidth="200" DockPanel.Dock="Right"/>
<TextBox IsReadOnly="True" DockPanel.Dock="Left">
<TextBox.Text>
<MultiBinding Converter="{StaticResource OutputConverter}">
<Binding ElementName="ListViewEntries" Path="SelectedItems" Mode="OneWay"/>
<!--<Binding Path="SelectedEntries" Mode="OneWay"/>-->
<Binding Path="SelectedCategory" Mode="OneWay"/>
<!-- Without it converter is not called after selection changes -->
<Binding Path="SelectedEntry" Mode="OneWay"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</DockPanel>
</Grid>
OutputConverter:
public class OutputConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
System.Collections.IList items = (System.Collections.IList)values[0];
var entries = items.Cast<Entry>();
TypeData type = values[1] as TypeData;
List<Entry> selectedEntries = new List<Entry>();
foreach (var entry in entries)
{
selectedEntries.Add(entry);
}
StringBuilder sb = new StringBuilder();
// ComboBox and Selection must not be empty
if (type != null && selectedEntries.Count > 0)
{
foreach (var selectedEntry in selectedEntries)
{
sb.AppendFormat("{0} {1}\n\n", selectedEntry.Author, type);
}
}
return sb.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I would suggest giving your Entry Class an IsSelected property (Bound to the IsSelectedProperty of a ListViewItem). When the selection changes you can just iterate through your collection (bound to the ListView) and check if they are selected or not. Like this (excuse the MvvmLight, RelayCommand = ICommand, ViewModelBase extends ObservableObject):
ViewModel:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
TestItemCollection = new ObservableCollection<TestItem>
{
new TestItem("Test1"),
new TestItem("Test2"),
new TestItem("Test3")
};
}
private TestItem m_selectedItemProperty;
public TestItem SelectedItemProperty
{
get
{
return m_selectedItemProperty;
}
set
{
m_selectedItemProperty = value;
RaisePropertyChanged("SelectedItemProperty");
}
}
public ObservableCollection<TestItem> TestItemCollection
{
get;
set;
}
public RelayCommand SelectionChanged
{
get { return new RelayCommand(OnSelectionChanged); }
}
private void OnSelectionChanged()
{
foreach (var item in TestItemCollection)
{
if (item.IsSelected)
Console.WriteLine("Name: " + item.Name);
}
}
}
I printed the names out here but you can also just add them to a string property and bind it to your text box or add the items to a collection (maybe bound to a ListBox or ItemsControl showing the selected entries).
TestItem:
public class TestItem : ObservableObject
{
public TestItem(string a_name)
{
m_name = a_name;
}
private string m_name;
public string Name
{
get
{
return m_name;
}
set
{
m_name = value;
RaisePropertyChanged("Name");
}
}
private bool m_isSelected;
public bool IsSelected
{
get
{
return m_isSelected;
}
set
{
m_isSelected = value;
RaisePropertyChanged("IsSelected");
}
}
}
View:
<Window x:Class="WpfAppTests.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:WpfAppTests"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
mc:Ignorable="d"
xmlns:modelNoMvvmLight="clr-namespace:WpfAppTests"
xmlns:modelMvvmLight="clr-namespace:WpfAppTests.ViewModel"
Title="MainWindow" Height="350" Width="525" >
<Window.DataContext>
<modelMvvmLight:MainViewModel/>
</Window.DataContext>
<StackPanel>
<ListView Name="ListView" ItemsSource="{Binding TestItemCollection}" SelectedItem="{Binding SelectedItemProperty}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChanged}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem" >
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
</StackPanel>
</Window>
You know when you stare at something long enough it doesn't really make sense any more... I am trying to bind the background property of a listview item to whether it is part of a collection in the viewmodel. Here is a simplified version of what I'm working with:
<Grid>
<ListView x:Name="AirportListView"
SelectionMode="None"
IsItemClickEnabled="True"
ItemClick="AirportListView_ItemClick">
<ListView.ItemTemplate>
<DataTemplate x:DataType="x:String">
<Grid Padding="16">
<TextBlock Text="{x:Bind}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
and:
public sealed partial class MainPage : Page
{
public ObservableCollection<string> MyAirports { get; set; } = new ObservableCollection<string>();
public MainPage()
{
this.InitializeComponent();
AirportListView.ItemsSource = new List<string>()
{
"EDDB",
"LGIR",
"EHAM",
"LFPO",
"EGKK"
};
}
private void AirportListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is string clickedAirport)
{
if (MyAirports.Contains(clickedAirport))
MyAirports.Remove(clickedAirport);
else
MyAirports.Add(clickedAirport);
}
}
}
Ideally what I would like to achieve is to bind the background of the grid in the datatemplate so that it is a different colour when an item is part of MyAirports. I just haven't been able to figure out how to do this using x:Bind or Binding. I can think of a few more long winded ways to achieve the same thing but wondered if there is perhaps a more elegant solution with data binding.
Any thoughts would be much appreciated!
Will
You need to make the collection that goes to ItemsSource a richer class than just a string. It should contain the property IsMyAirport, then it can be bound to each ListViewItem as well to update its background.
<Page ...
xmlns:local="using:UwpQuestions.Views"
xmlns:common="using:UwpQuestions.Common">
<Page.Resources>
<common:MyBgConverter x:Key="myBgConverter"/>
</Page.Resources>
<Grid>
<ListView x:Name="AirportList"
SelectionMode="None"
IsItemClickEnabled="True"
HorizontalContentAlignment="Stretch"
ItemsSource="{x:Bind Airports}"
ItemClick="AirportListView_ItemClick">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:AirportItem">
<Grid Padding="16" Width="150" Background="{x:Bind IsMyAirport, Mode=OneWay, Converter={StaticResource myBgConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{x:Bind Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Page>
With the following code behind:
public sealed partial class AirportListView : Page
{
public ObservableCollection<string> MyAirports { get; set; } = new ObservableCollection<string>();
public ObservableCollection<AirportItem> Airports { get; set; }
public AirportListView()
{
this.InitializeComponent();
Airports = new ObservableCollection<AirportItem>
{
new AirportItem {Name = "EDDB"},
new AirportItem {Name = "LGIR"},
new AirportItem {Name = "LFPO"},
new AirportItem {Name = "EGKK"}
};
}
private void AirportListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is AirportItem clickedAirport)
{
if (MyAirports.Contains(clickedAirport.Name))
{
MyAirports.Remove(clickedAirport.Name);
clickedAirport.IsMyAirport = false;
}
else
{
MyAirports.Add(clickedAirport.Name);
clickedAirport.IsMyAirport = true;
}
}
}
}
public class AirportItem : BindableBase
{
private string _name;
public string Name
{
get { return _name; }
set { SetProperty<string>(ref _name, value); }
}
private bool _isMyAirport = false;
public bool IsMyAirport
{
get { return _isMyAirport; }
set { SetProperty<bool>(ref _isMyAirport, value); }
}
}
AirportItem uses BindableBase to notify the ListView of when properties in the class change. It implements INotifyPropertyChanged. If you're not familiar with it, you should read up on XAML databinding.
And, it also uses the MyBgConverter (that Laith defined) to change the bool value into a different background color.
Finally, with the IsMyAirport property on the class, you may not need a separate MyAirports string list. I didn't remove it because I wasn't sure if you were using it for something else. But your logic in AirportListView_ItemClick can be changed to just use the IsMyAirport bool rather than the MyAirports list.
Create a converter that converts bool to a SolidColorBrush and use that in the binding.
<ListView Background="{x:Bind IsPartOfMyAirports, Converter={StaticResource MyBgConverter}}">
...
</ListView>
Your view model would be responsible for notifying the change for IsPartOfMyAirports.
Your converter would look something like this:
public class MyBgConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var b = (bool)value;
var color = b ? Colors.Yellow : Colors.Green;
return new SolidColorBrush(color);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
I'm implementing a user control in WPF using MVVM pattern. I want the control to contain an ItemsControl, specially a ComboBox, that contains a list of People. I want the first menu item to be labelled 'No Person' and bind to null on the data source, while the remaining items are names of people that bind to Person objects.
The code for Person and the view model is as follows:
namespace NoValueItem
{
public class Person : IEquatable<Person>
{
public int Id { get; set; }
public string Name { get; set; }
public static bool Equals(Person a, Person b)
{
if (a == null)
return b == null;
return a.Equals(b);
}
public bool Equals(Person other)
{
if (ReferenceEquals(this, other))
return true;
if (ReferenceEquals(null, other))
return false;
return this.Name.Equals(other.Name);
}
public override bool Equals(object obj)
{
return Equals(obj as Person);
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Person> ListOfPersons { get; } =
new ObservableCollection<Person> {
null,
new Person() { Id = 1, Name = "Alice" },
new Person() { Id = 2, Name = "Bob" },
new Person() { Id = 3, Name = "Charlie" }
};
private Person _SelectedPerson;
public Person SelectedPerson
{
get
{
return _SelectedPerson;
}
set
{
if (!Person.Equals(value, _SelectedPerson)) {
_SelectedPerson = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class NullSubstituteConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value ?? parameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (parameter == null)
return value;
return parameter.Equals(value) ? null : value;
}
}
}
And the view:
<Window x:Class="NoValueItem.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:NoValueItem"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid d:DataContext="{d:DesignInstance local:ViewModel}" VerticalAlignment="Center">
<Grid.DataContext>
<local:ViewModel/>
</Grid.DataContext>
<ComboBox ItemsSource="{Binding ListOfPersons}"
SelectedItem="{Binding SelectedPerson}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
At run-time, I get 4 items as I would expect: 1 blank menu item followed by 3 non-blank menu items, one for each Person in the ViewModel.ListOfPersons collection, with the item text bound to the Name property of the Person.
I would like the first blank item to instead show the text 'No Person'. How can I do this?
One thing I've tried is using the following data converter, that converts a null reference to the object specified in the converter parameter:
namespace NoValueItem
{
public class NullSubstituteConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value ?? parameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (parameter == null)
return value;
return parameter.Equals(value) ? null : value;
}
}
}
I then made the following changes to the view:
Added the NullSubstituteConverter from above as a static resource.
Added Person object as a static resources to represent the 'No Person' item and gave it the key 'NullPerson'.
Set the NullSubstituteConverter resource as the Converter for the binding for the SelectedItem property of the ComboBox.
Set the NullSubstituteConverter resource as the Converter for items in the data template for the ComboBox, so that the null item in the items source is converted to an the NullPerson object.
Here's the updated view:
<Window x:Class="NoValueItem.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:NoValueItem"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="300">
<Grid d:DataContext="{d:DesignInstance local:ViewModel}" VerticalAlignment="Center">
<Grid.Resources>
<local:Person x:Key="NullPerson">
<local:Person.Id>0</local:Person.Id>
<local:Person.Name>No Person</local:Person.Name>
</local:Person>
<local:NullSubstituteConverter x:Key="NullSubstituteConverter"/>
</Grid.Resources>
<Grid.DataContext>
<local:ViewModel/>
</Grid.DataContext>
<ComboBox ItemsSource="{Binding ListOfPersons}"
SelectedItem="{Binding SelectedPerson,
Converter={StaticResource NullSubstituteConverter},
ConverterParameter={StaticResource NullPerson}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock DataContext="{Binding Converter={StaticResource NullSubstituteConverter},
ConverterParameter={StaticResource NullPerson}}"
Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
This is closer to what I want. The blank menu item is now showing 'No Person', but there are still 2 problems:
When the view is first loaded, the 'No Person' item isn't automatically selected by default.
It's not possible to select the 'No Person' item.
I welcome any suggestions on how I can get the 'No Person' menu item working. It can be based on my approach above, or completely different approach as long as it works!
I'm working on adding some properties to an extension class of TreeView. I need the extra fields for context when one of the items in the tree is clicked. I can't seem to get the tree view to show any of the data I'm giving it.
In my MainView.cs I'm simply setting the items source as such:
TreeMenu.ItemsSource = (an ObservableCollection of ParentItems)
XAML:
<Grid x:Name="TreeGrid" Width="350" HorizontalAlignment="Left">
<TreeView Name="TreeMenu" Background="Red" Foreground="Black">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:ParentItem}" ItemsSource="{Binding ChildItems}">
<TextBlock Text="{Binding Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
Object model:
public class ParentItem : TreeViewItem, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string info)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
}
public ParentItem()
{
_text = "";
_url = "";
_childItems = new ObservableCollection<ChildItem>();
}
private string _text;
public string Text
{
get
{
return _text;
}
set
{
_text = value;
NotifyPropertyChanged("Text");
}
}
private string _url;
public string URL
{
get
{
return _url;
}
set
{
_url = value;
NotifyPropertyChanged("URL");
}
}
private ObservableCollection<ChildItem> _childItems;
public ObservableCollection<ChildItem> ChildItems
{
get
{
return _childItems;
}
set
{
_childItems = value;
NotifyPropertyChanged("ChildItems");
}
}
}
Note that ChildItem is almost identical to ParentItem, minus the collection object. Originally I tried extending TreeNode in my object classes, but that had the same issue.
Does anyone know why my TreeView won't appear? Am I missing something while extending TreeView?
No point in extending TreeViewItem.
We don't see how you assign your collection so they might something done wrongly.
This does work:
Code
using System.Collections.ObjectModel;
using System.Windows;
namespace WpfApplication4
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ItemCollection
{
new Item
{
Text = "A",
Items = new ItemCollection
{
new Item
{
Text = "B",
Items = new ItemCollection
{
new Item
{
Text = "C"
}
}
}
}
}
};
}
}
public class Item
{
public string Text { get; set; }
public ItemCollection Items { get; set; }
}
public class ItemCollection : ObservableCollection<Item>
{
}
}
XAML
<Window x:Class="WpfApplication4.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:WpfApplication4"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TreeView ItemsSource="{Binding}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="local:Item" ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
Result
I am wondering if it is possible to use two dictionaries in a listbox data template. I want to use the value of Names in a textbox, and the value of EnabledNames for a check box. Is this possible? If so, how would I go about doing it?
Some example data would be:
Dictionary<string, string> Names = new Dictionary<string, string>()
{
{ "4EG25","John" },
{"923OT", "Joe" }
};
Dictionary<string, bool> EnabledNames = new Dictionary<string, bool>()
{
{ "4EG25",false },
{"923OT", true}
};
And how I want to use in a way like:
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="359" VerticalAlignment="Top" Width="673" Margin="0,0,-0.333,-0.333" ItemsSource="{Binding Path=Names, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=EnabledNames[ItemsSource.Key].Value}" />
<TextBlock Text="{Binding Path=ItemsSource.Value}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Just create a class that contains both values and use it as the ItemsSource
class Name
{
public string Value { get; set; }
public bool Enabled { get; set; }
}
public IEnumerable<Name> TheNames
{
get { return Names.Select(n => new Name {Value = n.Value, Enabled = EnabledNames[n.Key]}); }
}
<ListBox ItemsSource="{Binding Path=TheNames, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=Enabled}" />
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is very hard, so I'd recommend you to create specific class as DataContext for your ListBox.
What you could try in your case:
WPF binding can be done only to properties (your dictionaries are now Fields, so your bindings won't work. Your ViewModel could be something like this:
public class ViewModel
{
public ViewModel()
{
Names = new Dictionary<string, string>()
{
{ "4EG25","John" },
{"923OT", "Joe" }
};
EnabledNames = new Dictionary<string, bool>()
{
{ "4EG25",false },
{"923OT", true}
};
}
public Dictionary<string, string> Names { get; set; }
public Dictionary<string, bool> EnabledNames { get; set; }
}
In your xaml when you set DataTemplate, its DataContext is set to single entry of your ItemsSource. In your case this is KeyValuePair. Also you could use MultiBinding to bind to your EnabledNames dictionary and use converter to convert your Key to bool Value from EnabledNames:
<Window x:Class="Test31641637.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Test31641637"
Title="MainWindow" Height="350" Width="525"
Name="mw">
<Window.Resources>
<ResourceDictionary>
<local:MultiDictionaryConverter x:Key="multiDictionaryConverter" />
</ResourceDictionary>
</Window.Resources>
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<ListBox x:Name="listBox" ItemsSource="{Binding Names}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox >
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource multiDictionaryConverter}" ConverterParameter="">
<Binding Path="Key" Mode="OneWay"/>
<Binding ElementName="mw" Path="DataContext.EnabledNames"/>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
<TextBlock Text="{Binding Path=Value}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
And IMultiValueConverter:
public class MultiDictionaryConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values.Length == 2 && values[0] is string && values[1] is Dictionary<string, bool>)
{/*
((Dictionary<string, bool>)values[1])[values[0] as string] =
!((Dictionary<string, bool>)values[1])[values[0] as string];*/
return ((Dictionary<string, bool>)values[1])[values[0] as string];
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
I suppose this is the easiest way to do it (but it is not easy) and it won't work, because when you click on CheckBox in your ListBox, ConvertBack method will be called, but it is impossible to convert back boolean value. So, again, the easiest way is to create specific class that represents single line of your ListBox:
public class Person
{
public string ID { get; set; }
public string Name { get; set; }
public bool IsEnabled { get; set; }
}