I am using AdvanceCollectionView from Windows Community Toolkit as a source for a XAML ListView, to allow sorting and filtering. I am having problems with updating the ListView.
To replicate the issue, I've created a simple Person class. In MainPage XAML I have a ListView MyXAMLList and a Button EditButton. In the MainPage code, I have an ObservableCollection<Person> MyPersonList and AdvancedCollectionView MyPersonACV. In Page_Loaded event I add a person to the list and use AdvancedCollectionView as a source for the list view:
Person p = new Person
{
Name = "John",
Age = 35
};
MyPersonList.Add(p);
MyPersonACV = new AdvancedCollectionView(MyPersonList, true);
MyXAMLList.ItemsSource = MyPersonACV;
This works and I can see John in the list.
In the EditButton code I try to update the item on the list but this isn't working. Both the ObservableCollection and the AdvancedCollectionView are updated, but the XAML list is still displaying the old name "John" instead of "Mary".
MyPersonList[0].Name = "Mary";
Debug.WriteLine(MyPersonList[0].ToString());
Debug.WriteLine(MyPersonACV[0].ToString());
I've tried updating the MyXAMLList.SelectedItem instead, but the same result:
Person p = (Person)MyXAMLList.SelectedItem;
p.Name = "Mary";
I've also tried adding MyPersonACV.Refresh(); but doesn't help.
What am I doing wrong? How can I update an item in the list?
Full code below
Person class:
class Person
{
public string Name {get; set;}
public int Age { get; set; }
public override string ToString()
{
return Name;
}
}
MainPage XAML:
<Page
x:Class="App3.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
Loaded="Page_Loaded">
<Grid>
<StackPanel Orientation="Vertical">
<ListView Height="Auto" Width="Auto" x:Name="MyXAMLList" SelectionMode="Single" IsItemClickEnabled="True"/>
<StackPanel Orientation="Horizontal">
<Button x:Name="EditButton" Content="Edit" Click="EditButton_Click"/>
</StackPanel>
</StackPanel>
</Grid>
</Page>
MainPage cs:
using Microsoft.Toolkit.Uwp.UI;
using System.Collections.ObjectModel;
using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace App3
{
public sealed partial class MainPage : Page
{
private ObservableCollection<Person> MyPersonList = new ObservableCollection<Person>();
private AdvancedCollectionView MyPersonACV;
public MainPage()
{
this.InitializeComponent();
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
//Change name
MyPersonList[0].Name = "Mary";
//Person p = (Person)MyXAMLList.SelectedItem;
//p.Name = "Mary";
Debug.WriteLine(MyPersonList[0].ToString());
Debug.WriteLine(MyPersonACV[0].ToString());
//MyPersonACV.Refresh();
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
//create person
Person p = new Person
{
Name = "John",
Age = 35
};
//add to list
MyPersonList.Add(p);
//set up ListView source
MyPersonACV = new AdvancedCollectionView(MyPersonList, true);
MyXAMLList.ItemsSource = MyPersonACV;
}
}
}
I noticed you override the ToString() method to display each item of ListView. When you update the Name property, even if the value of Name property has updated, since there is no binding relationship between Name property and ListViewItem, and the ToString() method isn't triggered when you update data, the UI isn't updated. It'sbetter to customize the appearance of items using DataTemplate, binding the Name property to the element(e.g. TetxBlock) and implement INotifyPropertyChanged interface. In this case, when the Name proeprty changes, it will provide change notifications to the binding and the UI will update. For exmaple:
.xaml:
<ListView Height="Auto" Width="Auto" x:Name="MyXAMLList" SelectionMode="Single" IsItemClickEnabled="True">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
.cs:
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string name { get; set; }
public string Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged();
}
}
public int Age { get; set; }
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
//Change name
MyPersonList[0].Name = "Mary";
}
Related
I'm using the MVVMLight Toolkit in my WPF project. All my ViewModels derive from the toolkit's ViewModelBase class, which implements the INotifyPropertyChanged for you and does all the notify work.
My current setup is extremely simple. I have a Person class with a single Name property.
public class Person
{
public string Name { get; set; }
}
My window has a TextBlock and a Button, and to the TextBlock I bind the Name property of the Person class object that I have. DataContext is set using a ViewModelLocator class.
<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:ignore="http://www.galasoft.ch/ignore"
mc:Ignorable="d ignore"
Height="300" Width="300"
Title="MVVM Light Application"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center"
Text="{Binding Contact.Name}"/>
<Button Grid.Row="1" Content="Click" Command="{Binding ClickCommand}"/>
</Grid>
</Window>
In my ViewModel, I set the Name to Tom in the constructor, and change it when the button is clicked. I expect Tom to show up in the TextBlock when window is loaded (which it does), and to be changed to Jane when the button is clicked (which it doesn't).
public class MainViewModel : ViewModelBase
{
private Person _contact = new Person();
public Person Contact
{
get { return _contact; }
set { Set(ref _contact, value); }
}
public RelayCommand ClickCommand { get; private set; }
public MainViewModel(IDataService dataService)
{
Contact = new Person() { Name = "Tom" };
ClickCommand = new RelayCommand(Click);
}
public void Click()
{
Contact.Name = "Jane";
}
}
What am I missing?
Setting Contact.Name does not trigger the INotifyPropertyChanged.NotifyChanged event as the Contact setter is not executed. You could fix this using by one of the following techniques:
Implement INotifyPropertyChanged also in your model class
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
if (PropertyChanged != null)
PropertyChanged(this, nameof(Name));
}
}
public event PropertyChangedHandler PropertyChanged;
}
Or wrap the PersonClass in a PersonViewModel
public class PersonViewModel : ViewModelBase
{
private readonly Person _person;
public PersonViewModel(Person person)
{
_person = person;
}
public string Name
{
get => _person.Name;
set
{
var name = _person.Name;
if (Set(ref name, value))
_person.Name = name;
}
}
}
and in MainViewModel:
private PersonViewModel _contactViewModel
public PersonViewModel Contact
{
get { return _contactViewModel ?? (_contactViewModel = new PersonViewModel(_contact)); }
}
Or create a separate ContactName property in the MainViewModel
... and using ContactName instead of Contact.Name in the binding and the Click event handler.
public string ContactName
{
get { return _contact.Name; }
set
{
var name = _contact.Name;
if (Set(ref name, value))
_contact.Name = name;
}
}
I'd like to access ComboBox items (which are defined in another class) in MainWindow.xaml.cs, but I can't.
I'm new to C# and WPF. The purpose of this code is to get a selected ComboBox item as a search key. I have copied from many example codes on the Internet and now I'm completely lost. I don't even know which part is wrong. So, let me show the entire codes (sorry):
MainWindow.xaml:
<Window x:Class="XY.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:XY"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="454.4">
<Grid>
<DataGrid ItemsSource="{Binding channels}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
Margin="0,0,0,-0.2">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding name}"
Header="Channel" Width="Auto"/>
<DataGridTemplateColumn Width="100" Header="Point Setting">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="piontsComboBox"
ItemsSource="{Binding DataContext.points,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
SelectionChanged="PrintText"
DisplayMemberPath="name"
SelectedValuePath="name"
Margin="5"
SelectedItem="{Binding DataContext.SelectedPoint,
RelativeSource={RelativeSource AncestorType={x:Type Window}},
Mode=TwoWay}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<TextBox x:Name="tb" Width="140" Height="30" Margin="10,250,200,30"></TextBox>
<Button x:Name="Browse_Button" Content="Browse" Margin="169,255,129.6,0"
Width="75" Click="Browse_Button_Click" Height="30" VerticalAlignment="Top"/>
</Grid>
MainWindow.xaml.cs:
using System;
using System.Windows;
using System.Windows.Controls;
namespace XY
{
public partial class MainWindow : Window
{
public GridModel gridModel { get; set; }
public MainWindow()
{
InitializeComponent();
gridModel = new GridModel();
this.DataContext = gridModel;
}
private void Browse_Button_Click(object sender, RoutedEventArgs e)
{
WakeupClass clsWakeup = new WakeupClass();
clsWakeup.BrowseFile += new EventHandler(gridModel.ExcelFileOpen);
clsWakeup.Start();
}
void PrintText(object sender, SelectionChangedEventArgs args)
{
//var item = pointsComboBox SelectedItem as Point;
//if(item != null)
//{
// tb.Text = "You selected " + item.name + ".";
//}
MessageBox.Show("I'd like to show the item.name in the TextBox.");
}
}
public class WakeupClass
{
public event EventHandler BrowseFile;
public void Start()
{
BrowseFile(this, EventArgs.Empty);
}
}
}
Point.cs:
namespace XY
{
public class Point : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _code;
public int code
{
get { return _code; }
set
{
_code = value;
OnPropertyChanged("code");
}
}
}
}
Record.cs:
namespace XY
{
public class Record : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _PointCode;
public int PointCode
{
get { return _PointCode; }
set
{
_PointCode = value;
OnPropertyChanged("PointCode");
}
}
private Record _selectedRow;
public Record selectedRow
{
get { return _selectedRow; }
set
{
_selectedRow = value;
OnPropertyChanged("SelectedRow");
}
}
private Point _selectedPoint;
public Point SelectedPoint
{
get { return _selectedPoint; }
set
{
_selectedPoint = value;
_selectedRow.PointCode = _selectedPoint.code;
OnPropertyChanged("SelectedRow");
}
}
}
}
ViewModelBase.cs:
using System.ComponentModel;
namespace XY
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
}
GridModel.cs:
using System.Collections.ObjectModel;
using System.Windows;
namespace XY
{
public class GridModel : ViewModelBase
{
public ObservableCollection<Record> channels { get; set; }
public ObservableCollection<Point> points { get; set; }
public GridModel()
{
channels = new ObservableCollection<Record>() {
new Record {name = "High"},
new Record {name = "Middle"},
new Record {name = "Low"}
};
}
internal void ExcelFileOpen(object sender, System.EventArgs e)
{
points = new ObservableCollection<Point> { new Point { } };
points.Add(new Point { name = "point1", code = 1 });
points.Add(new Point { name = "point2", code = 2 });
points.Add(new Point { name = "point3", code = 3 });
MessageBox.Show("Assume that Excel data are loaded here.");
}
}
}
The procedure goes like:
Click on the "Browse" button to load the data.
Click on the 1st column "Channel" to sort the list (This is a bug, but if you don't, the "Point Setting" items won't show up).
Click on the "Point Setting" ComboBox to select the items (point1, point2, ..., etc.).
This code is supposed to show the selected item name in the TextBox.
If everything is in MainWindow.xaml.cs, the ComboBox items could be accessed. Since I split the codes into different classes, it has not been working. Please help me. Any suggestion would be helpful.
Your binding does work. You can make use of the sender object to achieve what you wanted.
void PrintText(object sender, SelectionChangedEventArgs args)
{
var comboBox = sender as ComboBox;
var selectedPoint = comboBox.SelectedItem as Point;
tb.Text = selectedPoint.name;
}
The problem is that the DataGridColumn is not part of the WPF logical tree and so your relative source binding will not work. The only way to get your binding to work is a type of kluge (very common with WPF in my experience). Create a dummy element that is in the logical tree and then reference that.
So
<FrameworkElement x:Name="dummyElement" Visibility="Collapsed"/>
<DataGrid ItemsSource="{Binding channels}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
Margin="0,0,0,-0.2">
Then your binding will look like this
<ComboBox x:Name="piontsComboBox"
ItemsSource="{Binding DataContext.points,
Source={x:Reference dummyElement}}"
SelectionChanged="PrintText"
DisplayMemberPath="name"
SelectedValuePath="name"
Margin="5"
SelectedItem="{Binding DataContext.SelectedPoint,
Source={x:Reference dummyElement},
Mode=TwoWay}"/>
I'm building a WPF application to manage a student database stored in a SharePoint list. I'm new to using MVVM and have gone through a couple tutorials. I have gotten as far as having successfully created a view and model and have managed to bind it to a datagrid control. What I would like to do is to update the data in the view based on the output of a combobox.
Here's my model:
using System.ComponentModel;
namespace StudentManagement.Model
{
public class Student : INotifyPropertyChanged
{
private string _Title;
private string _FullName;
public string Title
{
get { return _Title; }
set
{
if (_Title != value)
{
_Title = value;
RaisePropertyChanged("Title");
}
}
}
public string FullName
{
get { return _FullName; }
set
{
if (_FullName != value)
{
_FullName = value;
RaisePropertyChanged("FullName");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Here's the viewmodel:
using System.Collections.ObjectModel;
using StudentManagement.Model;
using SP = Microsoft.SharePoint.Client;
using StudentManagement.publicClasses;
namespace StudentManagement.ViewModel
{
public class StudentViewModel
{
public ObservableCollection<Student> Students { get; set; }
public void LoadStudents(string query)
{
ObservableCollection<Student> _students = new
ObservableCollection<Student>();
SP.ClientContext ctx = clientContext._clientContext;
SP.CamlQuery qry = new SP.CamlQuery();
qry.ViewXml = query;
SP.ListItemCollection splStudents =
ctx.Web.Lists.GetByTitle("Students").GetItems(qry);
ctx.Load(splStudents);
ctx.ExecuteQuery();
foreach (SP.ListItem s in splStudents)
{
_students.Add(new Student { Title = (string)s["Title"], FullName = (string)s["FullName"] });
}
Students = _students;
}
}
}
Here's my XAML
<UserControl x:Class="StudentManagement.Views.StudentsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:local="clr-namespace:StudentManagement.Views" >
<Grid>
<StackPanel HorizontalAlignment="Left" Width="200" >
<TextBox Name="txtSearch" AcceptsReturn="True" ></TextBox>
<ComboBox Name="cmbStatus" SelectionChanged="cmbStatus_SelectionChanged" SelectedIndex="0">
<ComboBoxItem>Active</ComboBoxItem>
<ComboBoxItem>Inquiring</ComboBoxItem>
<ComboBoxItem>Inactive</ComboBoxItem>
<ComboBoxItem>Monitoring</ComboBoxItem>
</ComboBox>
<DataGrid Name="dgStudentList" ItemsSource="{Binding Path=Students}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
<DataGridTextColumn Header="Parent" Binding="{Binding FullName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="100" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
</UserControl>
...and the code behind for the view:
using System.Windows.Controls;
using StudentManagement.ViewModel;
namespace StudentManagement.Views
{
/// <summary>
/// Interaction logic for StudentsView.xaml
/// </summary>
public partial class StudentsView : UserControl
{
private StudentViewModel _viewModel = new
StudentViewModel();
public StudentsView()
{
InitializeComponent();
DataContext = _viewModel;
}
private void cmbStatus_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
string combotext = ((sender as ComboBox).SelectedItem as ComboBoxItem).Content as string;
string qry = #"<View>
<Query>
<Where><Eq><FieldRef Name='Current_x0020_Status' /><Value Type='Choice'>" + combotext + #"</Value></Eq></Where>
</Query>
</View>";
_viewModel.LoadStudents(qry);
}
}
}
As it stands now, the students get loaded into the datagrid on load just fine. When the event cmbStatus_SelectionChanged fires i've done tests and I can see that the LoadStudents function fires and returns the correct number of entries, but nothing gets updated on the datagrid.
I'm sure this a noob mistake and I'm missing something basic but this one is doing my head in and I'd appreciate any guidance.
Since StudentViewModel.LoadStudents() changes the value of the Students property, the view model needs to notify the view that this changed. You can do this by having StudentViewModel implement INotifyPropertyChanged (just like Student does). The DataGrid will subscribe to the PropertyChanged event, and will update its contents when that event is fired.
You are initializing your Students collection every time if the ComboBox's selection changed.
ObservableCollection<Student> _students = new ObservableCollection<Student>();
You should not do this with a bound collection in ViewModel. You can clear the collection and add new items like this.
public class StudentViewModel
{
public ObservableCollection<Student> Students { get; set; } = new ObservableCollection<Student>();
public void LoadStudents(string query)
{
Students.Clear();
SP.ClientContext ctx = clientContext._clientContext;
SP.CamlQuery qry = new SP.CamlQuery();
qry.ViewXml = query;
SP.ListItemCollection splStudents = ctx.Web.Lists.GetByTitle("Students").GetItems(qry);
ctx.Load(splStudents);
ctx.ExecuteQuery();
foreach (SP.ListItem s in splStudents)
{
Students.Add(new Student { Title = (string)s["Title"], FullName = (string)s["FullName"] });
}
}
}
I have a list of objects (Man) which each contains a Stack of states.
I have a Debug window which shows the selected Man's stack in a ListBox.
And I have a TabControl which I use to select a Man to debug.
To be able to select the correct binding, I made a property which returns the StateStack of the man at the selected index of the TabControl.
public object StateStack => Men[DebugIndex].States;
DebugIndex is bound to the TabControl's SelectedIndex property. So to make DebugIndex update the StateStack to show, I used OnPropertyChanged:
public int DebugIndex {
get => _debugIndex;
set {
_debugIndex = value;
OnPropertyChanged(nameof(StateStack));
}
}
The problem is, when the TabControl's SelectedIndex changes, the Stack is weirdly disordered! Bug the thing is that it's disordered only in the View, not really in the data.
I think it comes from something with the fact that I change the reference of the Binding it's an other Stack but I don't know how to solve that...
By the way, it works when I add all the Man objects and initialize their StateStack at the beginning. But as soon as I add a Man (and initialize its StateStack) later, for example when I click a Button, it doesn't work anymore...
public sealed partial class MainWindow : INotifyPropertyChanged {
private int _debugIndex;
public ObservableCollection<Man> Men { get; } = new ObservableCollection<Man>();
public MainWindow() {
Men.Add(new Man {Index = 0, States = new StateStack()});
InitializeComponent();
Men[0].States.Push(new State {Name = "Falling1"});
Men[0].States.Push(new State {Name = "Walking1"});
//this is simplified code. I push states here because in my program it's done during runtime (not during initialization)
}
public object StateStack => Men[DebugIndex].States;
public int DebugIndex {
get => _debugIndex;
set {
_debugIndex = value;
OnPropertyChanged(nameof(StateStack));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) {
Men.Add(new Man {Index = 1, States = new StateStack()});
Men[1].States.Push(new State {Name = "Falling2"});
Men[1].States.Push(new State {Name = "Walking2"});
Men[1].States.Push(new State {Name = "Running2"});
}
}
public class Man {
public int Index { get; set; }
public StateStack States { get; set; }
}
public class State {
public string Name { private get; set; }
public override string ToString() {
return Name;
}
}
public sealed class StateStack : Stack<State>, INotifyCollectionChanged {
public new void Push(State item) {
base.Push(item);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) {
CollectionChanged?.Invoke(this, e);
}
}
And my View code:
<Window x:Class="ObservableStackBug.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"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Add" Margin="5" Padding="8 2" HorizontalAlignment="Left" Click="ButtonBase_OnClick"/>
<ListBox ItemsSource="{Binding StateStack}" Grid.Row="1" />
<TabControl Grid.Row="2" ItemsSource="{Binding Men}" SelectedIndex="{Binding DebugIndex}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Index}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
</Window>
What could I do to say to my binding that when DebugIndex is changed, StateStack is a very other Stack?
I've simulated your scenario and observation is that there is problem with Push method for how the NotifyCollectionChangedEventArgs is item changed is propagated to source. The current code notifies that items are changed from the end index (but for stack the items are added at Top)). If you update the notification start index to 0 as NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0) then bound source will display the item in appropriate order in the view. You can read about NotifyCollectionChangedEventArgs here.
public new void Push(State item) {
base.Push(item);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0));
}
I've got a dilemma here. So I have mutiple expanders stacked on top of one another. Inside each expander is a ListBox, data bound, where each listitem displays a name of an object.
I've bound the search to filter the list items based on their name. However since I have two observable objects, the filtered items and unfiltered, the UI doesn't appear to get populated until someone searches. Whats the best way to fix this. I find it redundant to add items to both lists each time a new Person gets created. Using an mvvm approach.
The two collections are called People and PeopleFiltered. When i create people I add them to the list called People. When the search is applied it populates the PeopleFiltered list, which is the list the UI is bound to. How can I maintain this list be init to mimic People.
At the end of the day the PeopleFiltered collection should mimic People unless a search is being applied.
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="400" Width="200">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="Search:"/>
<TextBox Grid.Column="1" Background="Gold" Text="{Binding SearchString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<StackPanel Grid.Row="1">
<Expander Header="People" IsExpanded="{Binding IsExpanded, Mode=OneWay}">
<ListView ItemsSource="{Binding PeopleFiltered}">
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<Ellipse Width="8" Height="8" Fill="Green" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Name}"/>
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</StackPanel>
</Grid>
</Window>
MainWindowViewModel.cs
using System.Collections.ObjectModel;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Collections.Generic;
namespace WpfApplication1
{
public class MainWindowViewModel : NotifyBase
{
// search text
private string searchString;
public string SearchString
{
get { return searchString; }
set
{
this.searchString = value;
NotifyPropertyChanged("SearchString");
ApplySearchFilter();
}
}
private void ApplySearchFilter()
{
if (string.IsNullOrWhiteSpace(SearchString))
{
IsExpanded = false;
PeopleFiltered.Clear();
foreach (DisplayItem displayItem in People)
{
PeopleFiltered.Add(displayItem);
}
}
else
{
// open expanders and apply search
IsExpanded = true;
PeopleFiltered.Clear();
foreach (DisplayItem displayItem in People)
{
if (displayItem.Name.ToLowerInvariant().Contains(SearchString.ToLowerInvariant()))
{
PeopleFiltered.Add(displayItem);
}
}
}
}
// used to to open and close expanders
private bool isExpanded;
public bool IsExpanded
{
get { return this.isExpanded; }
set
{
this.isExpanded = value;
NotifyPropertyChanged("IsExpanded");
}
}
// data collections for each expander
private ObservableCollection<DisplayItem> people;
public ObservableCollection<DisplayItem> People
{
get { return people ?? (people = new ObservableCollection<DisplayItem>()); }
set
{
people = value;
NotifyPropertyChanged("People");
}
}
private ObservableCollection<DisplayItem> peopleFiltered;
public ObservableCollection<DisplayItem> PeopleFiltered
{
get { return peopleFiltered ?? (peopleFiltered = new ObservableCollection<DisplayItem>()); }
set
{
peopleFiltered = value;
NotifyPropertyChanged("PeopleFiltered");
}
}
// init
public MainWindowViewModel()
{
// People
People.Add(new DisplayItem() { Name="John" });
People.Add(new DisplayItem() { Name="Veta"});
People.Add(new DisplayItem() { Name="Sammy"});
People.Add(new DisplayItem() { Name = "Sarah" });
People.Add(new DisplayItem() { Name = "Leslie" });
People.Add(new DisplayItem() { Name = "Mike" });
People.Add(new DisplayItem() { Name = "Sherry" });
People.Add(new DisplayItem() { Name = "Brittany" });
People.Add(new DisplayItem() { Name = "Kevin" });
}
}
// class used to display all items
public class DisplayItem
{
public string Name { get; set; }
}
//observable object class
public class NotifyBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Here's a nice approach.
Create a Query property in your View Model, this will be bound to your filter TextBox.
private string _Query;
public string Query
{
get { return _Query; }
set
{
_Query = value;
Filter();
//Notify property changed.
}
}
One thing to note here is the Filter() method. This will be called every time the property changes, I'll get back to this later. Firstly, make sure your TextBox binding is TwoWay, it'll look like this:
<TextBox Text="{Binding Query}" ... />
In your View Model, you will need a collection for each ListBox.
private List<object> _Collection1; //The original collection
private List<object> _FilteredCollection1; //The filtered collection
public List<object> FilteredCollection1
{
get { return _FilteredCollection1; }
set
{
_FilteredCollection1 = value;
//Notify property changed.
}
}
//Some more collections
...
It's important to note here that there is a variable for the original unfiltered collection. This is important because we want to filter this list into a new collection, otherwise we'll just keep filtering over and over and eventually have nothing in the collection.
You'll need to bind the ItemsSource property in your ListBox to the collection(s).
<ListBox ItemsSource="{Binding FilteredCollection1}" ... />
Now, your Filter method can simply filter the _Collection1 variable into the FilteredCollection1 property.
private void Filter()
{
//Perform the filter here for all collections.
FilteredCollection1 = _Collection1.Where(x => x.Something == Query);
//Do the same for all other collections...
}
Note: The above Linq is just an example, I expect yours will be slightly more complicated than that, but you get the idea.
So. Whenever Query gets updated, the Filter collection will fire, update the FilteredCollection properties which will in turn call property changed and update the view.
Alternative Approach
Here's another way.
Instead of using a Filter method, you can instead put your filter code inside the get block in the FilteredCollection properties, like this:
public List<object> FilteredCollection1
{
get
{
return _Collection1.Where(...);
}
}
Then, in your Query property, simply call INotifyPropertyChanged for the collection:
private string _Query;
public string Query
{
get { return _Query; }
set
{
_Query = value;
//Notify property changed.
OnPropertyChanged("FilteredCollection1");
}
}
This will force the view to refresh the FilteredCollection1 property.