XAML binding custom CollectionViewSource Filter - c#

I'm trying to display a list of records by unit in separate columns. Everything is working except the binding to filter by unit.
I may be going about this completely the wrong way, but here's what I've got.
public class Record : INotifyPropertyChanged
{
public string Name { get; set; }
public string Unit { get; set; }
}
public class UnitList
{
public ObservableCollection<Record> Items { get; set; }
}
I'm using this markup extension code to do the filtering. In WPF can you filter a CollectionViewSource without code behind?
I can set the PropertyFilter Value statically and it filters fine, but all columns give me the same values. tbUnitName displays the correct value for Unit, but I can't get that same value passed in to the PropertyFilter Value I've tried the ProxyElement/FrameworkElement binding trick and as many different combinations of binding to parent elements as I can think of but I still get 'null' in the filter code.
In the window codebehind I've got:
List<string> names = (from s in this.SelectedSheets.Items
where !string.IsNullOrWhiteSpace(s.Unit)
select s.Unit).Distinct().ToList();
UnitNames = new ObservableCollection<string>(names);
lbUnits.ItemsSource = UnitNames;
Here's the XAML:
<ListBox Name="lbUnits" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Name="spRoster" Orientation="Horizontal" CanHorizontallyScroll="True" CanVerticallyScroll="False" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBox x:Name="tbUnitName" Text="{Binding Path=.}" />
<ListBox ItemsSource="{Binding }" ItemTemplate="{StaticResource listItemTemplate}" Margin="0,20,0,0" Name="lb1" HorizontalAlignment="Left" VerticalAlignment="Stretch" Width="150" ScrollViewer.CanContentScroll="False" >
<ListBox.DataContext>
<CollectionViewSource Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=Items}" >
<!--Filter="FilterOutB"-->
<CollectionViewSource.Filter>
<local:Filter>
<local:PropertyFilter PropertyName="Unit" Value="{Binding Path=.}" />
</local:Filter>
</CollectionViewSource.Filter>
</CollectionViewSource>
</ListBox.DataContext>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel MaxWidth="{Binding ActualWidth, ElementName=lb1}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I think I've solved it. I added
<Grid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</Grid.Resources>
and changed the filter to
<local:PropertyFilter PropertyName="Unit" Value="{Binding Source={StaticResource proxy}}" />

Related

How to show the checked items in my CheckBox

EDITED:
I've created three ListBoxes:
1st ListBox: listBox, which shows a list of U.S. States
2nd ListBox: listBox_Mirror, which contains a CheckBox to show the selected items of "listBox"
3rd ListBox: (No Name), which is supposed to show the checked items of "listBox_Mirror"
1st ListBox -> 2nd ListBox works fine. However, 2nd ListBox -> 3rd ListBox doesn't work, as you can see in the picture below:
Procedure:
Select all four items in the 1st ListBox.
Check the first two items (California and Illioni) in the 2nd ListBox's CheckBox.
See if California and Illioni are shown in the 3rd ListBox.
(In my case, nothing is shown.)
Here are my codes:
StateList.cs
using System.Collections.ObjectModel;
namespace CheckedListBox
{
public class StateList
{
public ObservableCollection<string> Data { get; }
public StateList()
{
Data = new ObservableCollection<string>();
Data.Add("California");
Data.Add("Illinoi");
Data.Add("Michigan");
Data.Add("New York");
}
}
}
MainWindow.xaml
<Window x:Class="CheckedListBox.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:CheckedListBox"
mc:Ignorable="d"
Title="MainWindow" Height="500" Width="400">
<Grid>
<ListBox ItemsSource="{Binding Path=Data}" x:Name="listBox" Margin="100,50,100,300" SelectionMode="Multiple"/>
<ListBox ItemsSource="{Binding SelectedItems, ElementName=listBox}" x:Name="listBox_Mirror" Margin="100,200,100,150">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<CheckBox Content="{TemplateBinding Content}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<ListBox ItemsSource="{Binding SelectedItems, ElementName=listBox_Mirror}" Margin="100,350,100,25"/>
</Grid>
MainWindow.xaml.cs
using System.Windows;
namespace CheckedListBox
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new StateList();
}
}
}
... I've changed the Binding property of the 3rd ListBox based on the IntelliSense's suggestion, but it doesn't work, too.
Please tell me how to fix this. Thank you.
Two things, there is no binding to the IsChecked and hence nothing ever is set. Also your data is just reference of strings; you should change it to a class with at least two properties, one a Boolean and the other the string you have now. Then bind appropriately.
Here is how to do it. I have a model which is defined in code behind but you can get the idea on its structure.
<Page ...
xmlns:model="clr-namespace:WPFStack.Model"/>
...
<Page.Resources>
<model:Orders x:Key="Orders">
<model:Order CustomerName="Alpha"
OrderId="997"
InProgress="True" />
<model:Order CustomerName="Beta"
OrderId="998"
InProgress="False" />
<model:Order CustomerName="Omega"
OrderId="999"
InProgress="True" />
<model:Order CustomerName="Zeta"
OrderId="1000"
InProgress="False" />
</model:Orders>
Now with my Listbox
<ListBox ItemsSource="{StaticResource Orders}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding CustomerName}"
IsChecked="{Binding InProgress, Mode=TwoWay}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Running it shows this:
Model
namespace WPFStack.Model
{
/// <summary>Class Orders which is a placeholder for Xaml example data.</summary>
public class Orders : List<Order> { }
public class Order
{
public string CustomerName { get; set; }
public int OrderId { get; set; }
public bool InProgress { get; set; }
}
}
Mirror
Ok, now I will name the controls lbOriginal and lbSelected to be accessible in the code behind. The new control lbSelected will mirror as such without directly connecting to the lbOriginal control or the data:
<ListBox x:Name="lbShowSelected">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding .}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Then I would subscribe to events such as Loaded, Checked and UnChecked on the original.
<ListBox x:Name="lbOriginal"
ItemsSource="{StaticResource Orders}"
Loaded="ProcessChange">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding CustomerName}"
IsChecked="{Binding InProgress, Mode=TwoWay}"
Checked="ProcessChange"
Unchecked="ProcessChange"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
So then at each step ProcessChange method properly updates the mirror (selected as I call it):
private void ProcessChange(object sender, RoutedEventArgs e)
{
if (lbOriginal.ItemsSource is Orders asOrders)
{
lbShowSelected.ItemsSource = null; // Inform control of reset
lbShowSelected.ItemsSource = asOrders.Where(ord => ord.InProgress)
.Select(ord => ord.CustomerName)
.ToList();
}
}
Then it is in sync and mirroring

ListVIew of ItemsControl: Get the ListViewItem index after selecting one its item from ItemsControl

I have a ListView. Inside of this ListView there is ItemsControl and inside it there is second ItemsControl. Inside of the second ItemsControl there are TextBoxes.
ListView -> ItemsControl -> ItemsControl -> TextBox
Is there any chance that I would be able to get index of ListViewItem, which specific TextBox belongs to after clicking on this TextBox?
For example
I select a ListViewItem on index 0 but then I click on TextBox which belong to ListViewItem on index 2. In that case I would like to change value of SelectedGroupIndex from 0 to 2.
"Hello" strings are just for testing.
Thank you very much.
ViewModel
public class MainWindowViewModel
{
public ObservableCollection<ObservableCollection<ObservableCollection<ListViewString>>> AllTexts { get; set; }
public int SelectedGroupIndex { get; set; }
public ICommand AddGroup { get; private set; }
public ICommand AddColumn { get; private set; }
public ICommand TextBoxSelected { get; private set; }
public MainWindowViewModel()
{
this.AllTexts = new ObservableCollection<ObservableCollection<ObservableCollection<ListViewString>>>();
this.SelectedGroupIndex = -1;
this.AddGroup = new Command(this.AddGroupCommandHandler);
this.AddColumn = new Command(this.AddColumnCommandHandler);
this.TextBoxSelected = new Command(this.TextBoxSelectedCommandHandler);
}
private void AddGroupCommandHandler()
{
var tempColumn = new ObservableCollection<ListViewString>() {
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello") };
var tempGroup = new ObservableCollection<ObservableCollection<ListViewString>>();
tempGroup.Add(tempColumn);
this.AllTexts.Add(new ObservableCollection<ObservableCollection<ListViewString>>(tempGroup));
}
private void AddColumnCommandHandler()
{
if (this.SelectedGroupIndex >= 0 && this.SelectedGroupIndex < this.AllTexts.Count)
{
var tempColumn = new ObservableCollection<ListViewString>() {
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello"),
this.GetListViewString("Hello") };
this.AllTexts[this.SelectedGroupIndex].Add(tempColumn);
}
}
private void TextBoxSelectedCommandHandler()
{
// TODO: Change SelectedItem of ListView
// this.SelectedGroupIndex = ...;
}
private ListViewString GetListViewString(string text)
{
return new ListViewString { Value = text };
}
private string GetTextFromListViewString(ListViewString listViewString)
{
return listViewString.Value;
}
}
/// <summary>
/// Class used to show user Text in ListView.
/// Using this class fixes the issue that ObservableCollection didn't update
/// after user changed values of TextBoxes in GUI.
/// </summary>
public class ListViewString : DependencyObject
{
public string Value
{
get
{
return (string)GetValue(ValueProperty);
}
set
{
SetValue(ValueProperty, value);
}
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(string), typeof(ListViewString), new PropertyMetadata(string.Empty));
}
View:
<Window.Resources>
<ResourceDictionary>
<local:MainWindowViewModel x:Key="vm" />
</ResourceDictionary>
</Window.Resources>
<Grid Margin="10,10,10,10" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="300" />
<RowDefinition />
</Grid.RowDefinitions>
<ListView Grid.Row="0"
ItemsSource="{Binding AllTexts, Source={StaticResource vm}, Mode=TwoWay}"
Background="Blue"
SelectedIndex="{Binding SelectedGroupIndex, Source={StaticResource vm}}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Value}"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Center"
Width="100" Height="40">
<TextBox.InputBindings>
<MouseBinding Gesture="LeftClick"
Command="{Binding TextBoxSelected, Source={StaticResource vm}}" />
</TextBox.InputBindings>
</TextBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,20,0,0">
<Button Content="Add Group" Width="120" Height="30"
Command="{Binding AddGroup, Source={StaticResource vm}}" />
<Button Content="Add Column" Margin="20,0,0,0" Width="120" Height="30"
Command="{Binding AddColumn, Source={StaticResource vm}}" />
<TextBlock Width="120" Height="30" FontSize="20" Margin="20,0,0,0"
Text="{Binding SelectedGroupIndex, Source={StaticResource vm}}" />
</StackPanel>
</Grid>
Actually what you need is to pass the DataContext of your TextBox to the command as parameter, so use CommandParameter for it and implement your command with parameter:
<TextBox.InputBindings>
<MouseBinding Gesture="LeftClick"
Command="{Binding TextBoxSelected, Source={StaticResource vm}}"
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Mode=Self}}"/>
</TextBox.InputBindings>
So you will have an item from your items source collection as command parameter and can find the index of it.

ToolTip use to show the index of an item from a collection. C# WPF MVVM

I'm plotting a histogram of a grayscale image using an array of 256 values. Im doing so by creating my own chart with 256 vertical rectangles (columns). My aim is that when I mouse over one rectangle I get its index value, its index from the array it lies in, like if I mouse over the 200th rectangle I get 200 in a small text box near cursor, like how ToolTip should perform. The problem is that I don't find the proper binding for ToolTip to get this working. I think the solution lies in the proper use of AlternationCount / AlternationIndex
Here is may XAML code, maybe someone can give me a fix for it:
<ItemsControl Grid.Row="1" ItemsSource="{Binding HistogramValues}" AlternationCount="{Binding Path=HistogramValues.Count}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Height="{Binding }" ToolTip="{Binding AlternationIndex, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Width="2" VerticalAlignment="Bottom" Fill="Black"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
where in view model I have:
public float[] HistogramValues { get; set; }
I've found this useful post
Numbered listbox
but I still cant make it run for my case, I'm confused about that ItemTemplate and TemplatedParent which I dont know if I need or if I need the template how should I code this.
If you are not ok with converters you can use this,
<ItemsControl Grid.Row="1" ItemsSource="{Binding Collection}" AlternationCount="{Binding Path=Collection.Count}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" ToolTip="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentPresenter}}">
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Since AlternationIndex is attached property, it should be covered with "()".
If you dont specify source in a binding, it always binds to datacontext.
Do this
ToolTip="{Binding (ItemsControl.AlternationIndex), RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
If showing the index is your only requirement you can achieve this like below,,
MyConverter is a multiconverter ,and include that in resources
<ItemsControl Grid.Row="1" ItemsSource="{Binding Collection}" >
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Control.ToolTip">
<Setter.Value>
<MultiBinding Converter="{StaticResource MyConverter}">
<Binding Path="IsMouseOver" RelativeSource="{RelativeSource Self}"/>
<Binding />
<Binding Path="ItemsSource" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ItemsControl}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
public partial class MainWindow : Window
{
private ObservableCollection<string> collection;
public ObservableCollection<string> Collection
{
get { return collection; }
set { collection = value; }
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
collection = new ObservableCollection<string>();
collection.Add("First");
collection.Add("Second");
collection.Add("Third");
collection.Add("Fourth");
}
}
public class MyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((bool)values[0])
{
return (values[2] as ObservableCollection<string>).IndexOf(values[1].ToString());
}
else
return "";
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

How to have recursion, or a hierarchy lists and sublists in XAML?

Say I have the following class, Employee
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public ObservableCollection<Employee> Underlings { get; set; }
}
And then I have the following XAML, bound to an ObservableCollection<Employee> MyEmployees
<ListBox ItemsSource="{Binding Path=MyEmployees}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Tag="{Binding Path=Employee.Id}">
<TextBlock Text="{Binding Path=Employee.Name}"></TextBlock>
<!-- Here's where I declare my underlings -->
<ListBox ItemsSource="{Binding Path=Employee.Underlings}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Tag="{Binding Path=Employee.Id}">
<TextBlock Text="{Binding Path=Employee.Name}"></TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This allows each employee in the collection MyEmployees to have some underlings. But those underlings are also of type employee, and could have their own underlings. How do I cater for those additional levels without making my XAML very complex?
Is there some way to declare my DataTemplate separately and allow it to be referenced within itself?
Do I have to do all this from code-behind instead?
(I realise the XAML above may not be 100% correct, its just an example)
therefore you have to use a TreeView and not a ListBox. And You have to specify a HierarchicalDataTemplate.
You could define the DataTemplate inside the ListBoxes Resources as the default template for Employees:
<ListBox ItemsSource="{Binding MyEmployees}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type myns:Employee}">
<Grid Tag="{Binding Id}">
<TextBlock Text="{Binding Name}"></TextBlock>
<ListBox ItemsSource="{Binding Underlings}" />
</Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>

How to bind an enum ItemsControl to a Collection in WPF?

I want to have a list of checkboxes that binds to a collection. So when options are selected, they are added to the list --- when options are deselected, they get removed.
Have tried a number of approaches, but failed to solve this.
Model
public enum WeatherType
{
Rainy,
Sunny,
Cloudy,
Windy
}
ViewModel
public class WeatherViewModel : INotifyPropertyChanged
{
public ObservableCollection<WeatherType> WeatherTypes {get;set;}
...
}
XAML
<ObjectDataProvider x:Key="weather"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="business:WeatherType" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
...
<ItemsControl Grid.Row="4"
Grid.Column="1"
ItemsSource="{Binding Source={StaticResource weather}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="{Binding}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Declare a view model for weather type:
public class WeatherTypeViewModel
{
public WeatherType WeatherType { get; set; }
public bool IsChecked { get; set; }
}
Change your view model like this:
public class WeatherViewModel : INotifyPropertyChanged
{
public ObservableCollection<WeatherTypeViewModel> WeatherTypes {get;set;}
...
}
and view - like this:
<ItemsControl Grid.Row="4"
Grid.Column="1"
ItemsSource="{Binding WeatherTypes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="{Binding WeatherType}" IsChecked="{Binding IsChecked}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Categories

Resources