Today I am stucked in very basic concept again. What is the mistake I am doing.
I have XAML like
<ComboBox ItemsSource="{Binding MyItems}" Height="40" Width="200" SelectedIndex="0"
SelectedItem="{Binding MySelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding MySelectedItem.Name}"/>
<Button Content="Test" Grid.Row="1" Click="Button_Click_1"/>
My ModelView looks like
public class MainViewModel : DependencyObject,INotifyPropertyChanged
{
private MyModel mySelectedItem;
public MyModel MySelectedItem
{
get
{
return mySelectedItem;
}
set
{
if (value != mySelectedItem)
{
mySelectedItem = value;
RaisePropertyChange("MySelectedItem");
}
}
}
public IList<MyModel> MyItems
{
get
{
return new List<MyModel>() {new MyModel(){Name="A"},
new MyModel(){Name="B"},
new MyModel(){Name="C"}};
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChange(string name)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
One MyItems property and one SelectedItem property
and Click handler like
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Random r = new Random();
int icnt = r.Next(0,3);
model.MySelectedItem = model.MyItems[icnt];
}
I found that TextBlock.Text is updating but Combobox selected item is not updating. I try to dig out the reason and found that if I execute code below
MyModel prevItem = model.MyItems.Where((m) => m.Name.Equals("A")).FirstOrDefault();
MyModel newItem = model.MyItems.Where((m) => m.Name.Equals("A")).FirstOrDefault();
bool result = prevItem.Equals(newItem);
The value is always false. But why, why I am getting the new reference to same object from collection.
How can resolve this issue.
Thanks
you are getting a new reference because each time the binding mechanism will ask for MyItems you will create a new list.
try creating it once and use observable collection
You need to modify your MyItems code. You are getting new list every time. Try this out.
private List<MyModel> _myItems;
public IList<MyModel> MyItems
{
get
{
if (_myItems == null)
{
myItems = new List<MyModel>();
myItems.Add(new MyModel() { Name = "A" });
myItems.Add(new MyModel() { Name = "B" });
myItems.Add(new MyModel() { Name = "C" });
}
return _myItems}
}
}
Related
I've got ComboBox and ListBox of CheckBoxes. Depending on SelectedItem of ComboBox ItemSource of ListBox must change. I made a sample to make thing easier. Here is the code:
ViewModel
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace Test
{
class Data
{
public long Id;
public object Value;
public override string ToString()
{
return Value.ToString();
}
}
class CheckedData: INotifyPropertyChanged
{
private Data myData;
public Data MyData
{
get { return myData; }
set
{
if (myData == value)
return;
myData = value;
RaisePropertyChanged(nameof(MyData));
}
}
private bool isChecked;
public bool IsChecked
{
get { return isChecked; }
set
{
isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class BindingObject: INotifyPropertyChanged
{
private ObservableCollection<Data> dataList = new ObservableCollection<Data>();
public ObservableCollection<Data> DataList
{
get { return dataList; }
set
{
dataList = value;
RaisePropertyChanged(nameof(DataList));
}
}
private Data selectedItem;
public Data SelectedItem
{
get { return selectedItem; }
set
{
if (value == selectedItem)
return;
selectedItem = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class ViewModel: INotifyPropertyChanged
{
public ViewModel()
{
var tmp = new Data() {Id = 1, Value = "Cat"};
Obj.DataList.Add(tmp);
Obj.SelectedItem = tmp;
Obj.DataList.Add(new Data() {Id = 2, Value = "Dog"});
Mapping[1] = new ObservableCollection<CheckedData>()
{
new CheckedData() {IsChecked = true, MyData = new Data() {Id = 1, Value = "Maine coon"}},
new CheckedData() {IsChecked = true, MyData = new Data() {Id = 2, Value = "Siberian"}}
};
}
private BindingObject obj = new BindingObject();
public BindingObject Obj
{
get { return obj; }
set
{
if (obj == value)
return;
obj = value;
RaisePropertyChanged(nameof(Obj));
}
}
private Dictionary<long, ObservableCollection<CheckedData>> mapping = new Dictionary<long, ObservableCollection<CheckedData>>();
public Dictionary<long, ObservableCollection<CheckedData>> Mapping
{
get { return mapping; }
set
{
if (mapping == value)
return;
mapping = value;
RaisePropertyChanged(nameof(Mapping));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
View
<ComboBox x:Name="comboBox" ItemsSource="{Binding Path=Obj.DataList}" SelectedItem="{Binding Path=Obj.SelectedItem, Mode=TwoWay}"/>
<ListBox x:Name="listBox" Height="100" ItemsSource="{Binding Path=Mapping[Obj.SelectedItem.Id]}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}" Content="{Binding Path=MyData.Value}" Margin="0,5,5,0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
That is what I thought should work. ComboBox is okay, but ListBox ItemSource binding doesn't work. Only if I bind directly to list like this:
ViewModel
private ObservableCollection<CheckedData> test = new ObservableCollection<CheckedData>()
{
new CheckedData() {IsChecked = true, MyData = new Data() {Id = 1, Value = "Maine coon"}},
new CheckedData() {IsChecked = false, MyData = new Data() {Id = 2, Value = "Siberian"}}
};
public ObservableCollection<CheckedData> Test
{
get { return test; }
set
{
test = value;
RaisePropertyChanged(nameof(Test));
}
}
View
<ListBox x:Name="listBox" Height="100" ItemsSource="{Binding Path=Test}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}" Content="{Binding Path=MyData.Value}" Margin="0,5,5,0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Everything starts working.. Except Content binding, because I can't go deeper than 1 level of property path. So I have to override ToString() method in Data.
What should I fix to make everything work? Am I able to bind ItemSource like this? Why can't I go deeper than 1 lvl property binding in CheckBox?
Am I able to bind ItemSource like this?
No, this kind of bindings are not supported in XAML:
Binding Path=Mapping[Obj.SelectedItem.Id].
You must replace Obj.SelectedItem.Id with a constant key value like 1 or bind to some other property that returns the collection of items.
Everything starts working.. Except Content binding
You can only bind to public properties so Value must be a property and not a field:
class Data
{
public long Id { get; set; }
public object Value { get; set; }
}
You can achive this easy with:
public ObservableCollection<CheckedData> SelectedData
{
get
{
return Mapping[Obj.SelectedItem.Id];
}
}
And into
public Data SelectedItem
{
get { return selectedItem; }
set
{
if (value == selectedItem)
return;
selectedItem = value;
RaisePropertyChanged(nameof(SelectedData)); // add this.
}
}
Now, in XAML, you can easy:
<ListBox x:Name="listBox" Height="100" ItemsSource="{Binding Path=Obj.SelectedData}">
I have a List of Lists and display it with nested ListBoxes:
MainWindow.xaml.cs
using System.Collections.Generic;
namespace WPF_Sandbox
{
public partial class MainWindow
{
public IEnumerable<IEnumerable<string>> ListOfStringLists { get; set; } = new[] { new[] { "a", "b" }, new[] { "c", "d" } };
public MainWindow()
{
InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
// do something with all selected items
};
}
}
}
MainWindow.xaml
<Window x:Class="WPF_Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
x:Name="ThisControl">
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
</Window>
How can I get all selected items across all ListBoxes?
I found a few solutions getting one selected item but could not figure out how to do applie those in my scenario.
I have an idea on how to do this by wrapping the string arrays but I would prefer not doing this.
I would just add an event handler to the inner ListBox like so if not doing things the MVVM way:
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple" SelectionChanged="ListBox_SelectionChanged">
Then in your code behind implement the ListBox_SelectionChanged like so:
public List<string> FlatStringList = new List<string>();
private void ListBox_SelectionChanged(object sender,System.Windows.Controls.SelectionChangedEventArgs e)
{
FlatStringList.AddRange(e.AddedItems.Cast<string>());
foreach(string s in e.RemovedItems)
{
FlatStringList.Remove(s);
}
}
This is assuming you don't mind storing the selected strings in a flat list. Then you could implement your DoSomething button click event handler to do something with the FlatStringList.
Hope that helps.
The easiest way would be to iterate through the items in the ListBoxes:
private void DoSomethingButton_Click(object sender, RoutedEventArgs e)
{
List<string> selectedStrings = new List<string>();
foreach (IEnumerable<string> array in outerListBox.Items.OfType<IEnumerable<string>>())
{
ListBoxItem lbi = outerListBox.ItemContainerGenerator.ContainerFromItem(array) as ListBoxItem;
if (lbi != null)
{
ListBox innerListBox = GetChildOfType<ListBox>(lbi);
if (innerListBox != null)
{
foreach (string selectedString in innerListBox.SelectedItems.OfType<string>())
selectedStrings.Add(selectedString);
}
}
}
}
private static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null)
return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null)
return result;
}
return null;
}
Note that the ListBoxItem may be virtualized away if you have a lot of inner IEnumerable<string>. You will then have to force the generation of the containers or disable UI virtualization:
WPF ListView virtualization. How to disable ListView virtualization?
This may affect the performance negatively so if this is an issue you should probably consider binding to an IEnumerable<YourType> and bind the SelectedItems property of the inner ListBox to a property of a YourType using a behaviour.
Since the SelectedItems property of a ListBox is read-only you can't bind to it directly: https://blog.magnusmontin.net/2014/01/30/wpf-using-behaviours-to-bind-to-readonly-properties-in-mvvm/.
Why don't you create a wrapper (as you said):
public class MyString : INotifyPropertyChanged
{
public MyString(string value) { Value = value; }
string _value;
public string Value { get { return _value; } set { _value = value; RaisePropertyChanged("Value"); } }
bool _isSelected;
public bool IsSelected { get { return _isSelected; } set { _isSelected = value; RaisePropertyChanged("IsSelected"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
Bind the IsSelected property of the ListBoxItems:
<StackPanel>
<ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding Value}" />
</ItemContainerTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</ItemContainerTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Name="DoSomethingButton" Content="DoSomething" />
</StackPanel>
and you are already done:
public IEnumerable<IEnumerable<MyString>> ListOfStringLists { get; set; } = new[] { new[] { new MyString("a"), new MyString("b") { IsSelected = true } }, new[] { new MyString("c"), new MyString("d") } };
public MainWindow()
{
this.InitializeComponent();
DoSomethingButton.Click += (sender, e) =>
{
foreach (var i in ListOfStringLists)
foreach (var j in i)
{
if (j.IsSelected)
{
// ....
}
}
};
}
I have a group class which contains a list of items and a header:
public class MyGroup {
public MyGroup(string _header){
header = _header;
}
protected string header = "";
public string Header
{
get { return header; }
}
protected List<MyGroupItem> item = new List<MyGroupItem>();
public List<MyGroupItem> Item
{
get { return item; }
}
}
public class MyGroupItem {
public MyGroupItem(string _name, double _multiplier){
name = _name;
multiplier = _multiplier;
}
protected double multiplier = 1.0;
protected string name = "";
public string Name
{
get { return name; }
}
}
So far so good. In my main class, I have an observable collection of groups and I populate it like this:
protected ObservableCollection<MyGroup> groups = new ObservableCollection<MyGroup>();
public ObservableCollection<MyGroup> Groups
{
get { return groups; }
}
protected MyGroupItem currentItem;
public MyGroupItem CurrentItem
{
get { return currentItem; }
set
{
if (currentItem== value) return;
currentItem= value;
NotifyPropertyChanged("CurrentItem");
}
}
....
var GroupA = new MyGroup("Group A");
GroupA.MyGroupItem.Add("Item 1", 1.0);
Groups.Add(GroupA);
currentItem = GroupA.MyGroupItem[0];
All of the above simply shows how I've setup my classes and observable lists. Now, I switch over to the xaml.
<ItemsControl ItemsSource="{Binding Path=Groups}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Background="Transparent" ClipToBounds="True" Orientation="Vertical"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:MyGroup}">
<StackPanel>
<TextBlock Text="{Binding Path=Header}"></TextBlock>
<ListView ItemsSource="{Binding Path=MyGroupItem}" SelectedItem="{Binding Path=DataContext.CurrentItem, ElementName=ControlRoot}">
<ListView.ItemTemplate>
<DataTemplate DataType="local:MyGroupItem">
<TextBlock Text="{Binding Path=Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
So, basically I have a ItemControl that displays multiple lists of items. The control should place the header for the name of the group, and then show a list view of the particular items within that group. The layout works perfectly... however, the issue comes when I deal with the Selected Item. Basically, the list view allows me to select an item within one of the multiple groups... meaning that I may have multiple items selected at any given time. For example, lets say that I select the first item in Group A. But, then I change my selection to the second item in Group B. Because Group B is a separate list, it allows me to activate that item... but it doesn't deselect the item in Group A. What I'd like is that this multi-list group to act as a single list. Is this possible? Do I need to setup a separate SelectionChanged event? And if so, how would I go about making sure when the selection is changed that it clears the selected items from all lists and only shows the correct one that the user just selected?
You should handle this in your view model classes.
If you add a property to hold the selected item of each group to the MyGroup class and implement the INotifyPropertyChanged interface, you could handle the CollectionChanged event of the Groups collection in the view model class to set the CurrentItem property and at the same time clear the SelectedItem property of the other groups by setting it to null in this event handler.
Here is an example for you.
MyGroup.cs:
public class MyGroup : INotifyPropertyChanged
{
public MyGroup(string _header)
{
header = _header;
}
protected string header = "";
public string Header
{
get { return header; }
}
protected List<MyGroupItem> item = new List<MyGroupItem>();
public List<MyGroupItem> Item
{
get { return item; }
}
private MyGroupItem _item;
public MyGroupItem SelectedItem
{
get { return _item; }
set { _item = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
MyGroupItem.cs:
public class MyGroupItem
{
public MyGroupItem(string _name, double _multiplier)
{
name = _name;
multiplier = _multiplier;
}
protected double multiplier = 1.0;
protected string name = "";
public string Name
{
get { return name; }
}
}
View Model:
public class Window1ViewModel : INotifyPropertyChanged
{
public Window1ViewModel()
{
groups.CollectionChanged += (s, e) =>
{
if (e.NewItems != null)
{
foreach (object item in e.NewItems)
{
(item as INotifyPropertyChanged).PropertyChanged
+= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null)
{
foreach (object item in e.OldItems)
{
(item as INotifyPropertyChanged).PropertyChanged
-= new PropertyChangedEventHandler(item_PropertyChanged);
}
};
};
var GroupA = new MyGroup("Group A");
GroupA.Item.Add(new MyGroupItem("Item 1", 1.0));
GroupA.Item.Add(new MyGroupItem("Item 2", 1.0));
GroupA.Item.Add(new MyGroupItem("Item 3", 1.0));
Groups.Add(GroupA);
var GroupB = new MyGroup("Group B");
GroupB.Item.Add(new MyGroupItem("Item 1", 1.0));
GroupB.Item.Add(new MyGroupItem("Item 2", 1.0));
GroupB.Item.Add(new MyGroupItem("Item 3", 1.0));
Groups.Add(GroupB);
currentItem = GroupA.Item[0];
}
private bool _handle = true;
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (!_handle)
return;
MyGroup group = sender as MyGroup;
CurrentItem = group.SelectedItem;
//clear the selection in the other groups:
_handle = false;
foreach (MyGroup g in Groups)
if (g != group)
g.SelectedItem = null;
_handle = true;
}
protected ObservableCollection<MyGroup> groups = new ObservableCollection<MyGroup>();
public ObservableCollection<MyGroup> Groups
{
get { return groups; }
}
protected MyGroupItem currentItem;
public MyGroupItem CurrentItem
{
get { return currentItem; }
set
{
if (currentItem == value) return;
currentItem = value;
NotifyPropertyChanged("CurrentItem");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
View:
<ItemsControl ItemsSource="{Binding Path=Groups}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Background="Transparent" ClipToBounds="True" Orientation="Vertical"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:MyGroup}">
<StackPanel>
<TextBlock Text="{Binding Path=Header}"></TextBlock>
<ListView ItemsSource="{Binding Path=Item}"
SelectedItem="{Binding SelectedItem}">
<ListView.ItemTemplate>
<DataTemplate DataType="local:MyGroupItem">
<TextBlock Text="{Binding Path=Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I have two different objects that are pointing at each other. The first object represents a division in a company. That object has two collection: Employees, which is all the employees working in the division and Project, which is all the special projects that are in progress within that division. So the first object looks like this:
public class Division : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
ObservableCollection<Employee> _employees;
ObservableCollection<Project> _projects;
public Division()
{
Employees = new ObservableCollection<Employee>();
Projects = new ObservableCollection<Project>();
}
public ObservableCollection<Employee> Employees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
}
}
}
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
if (_projects != value)
{
_projects = value;
PropertyChanged(this, new PropertyChangedEventArgs("Projects"));
}
}
}
public void AddNewProject()
{
this.Projects.Add(new Project(this));
}
}
Notice that when adding a new project to the division, I pass a reference to the division into that project, which looks like this:
public class Project : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
string _projectName;
DateTime _deadline = DateTime.Now;
Division _division;
ObservableCollection<Employee> _members;
public Project()
{
Members = new ObservableCollection<Employee>();
}
public Project(Division div)
{
Members = new ObservableCollection<Employee>();
Division = div;
}
public string ProjectName
{
get { return _projectName; }
set
{
if (_projectName != value)
{
_projectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ProjectName"));
}
}
}
public DateTime Deadline
{
get { return _deadline; }
set
{
if (_deadline != value)
{
_deadline = value;
PropertyChanged(this, new PropertyChangedEventArgs("Deadline"));
}
}
}
public Division Division
{
get { return _division; }
set
{
if (_division != value)
{
if (_division != null)
{
_division.Employees.CollectionChanged -= members_CollectionChanged;
}
_division = value;
if (_division != null)
{
_division.Employees.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Division"));
}
}
}
public ObservableCollection<Employee> Members
{
get { return _members; }
set
{
if (_members != value)
{
if (_members != null)
{
_members.CollectionChanged -= members_CollectionChanged;
}
_members = value;
if (_members != null)
{
_members.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Members"));
}
}
}
public ObservableCollection<Employee> AvailableEmployees
{
get
{
if (Division != null){
IEnumerable<Employee> availables =
from s in Division.Employees
where !Members.Contains(s)
select s;
return new ObservableCollection<Employee>(availables);
}
return new ObservableCollection<Employee>();
}
}
void members_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableEmployees"));
}
}
The reason I'm doing it like this is, that the project could have any type of team working on it, but only from within the division. So, when building a dashboard for the division, the manager could select any of the employees to that project but without putting in an employee that is already assigned to it. So, the AvailableEmployees property in the project object always keeps track of who is not already assigned to that project.
The problem I'm having is how to translate this into a UI. The experiment I've done so far looks like this:
<UserControl x:Class="Test.Views.TestView"
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:Test.Views"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<ListBox ItemsSource="{Binding Div.Projects}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent"
BorderThickness="0, 0, 0, 2"
BorderBrush="Black"
Margin="0, 0, 0, 5"
Padding="0, 0, 0, 5">
<StackPanel>
<TextBox Text="{Binding ProjectName}"/>
<ListBox ItemsSource="{Binding Members}">
<ListBox.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add Employee to Project"
Command="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AddEmployeeToProject}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add New Project"
Command="{Binding AddNewProject}" />
</StackPanel>
The view model associated with this view is as follows:
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private Division _div;
public TestViewModel(Division div)
{
Div = div;
AddNewProject = new DelegateCommand(OnAddNewProject);
AddEmployeeToProject = new DelegateCommand<Project>(OnAddEmployeeToProject);
}
public DelegateCommand AddNewProject { get; set; }
public DelegateCommand<Project> AddEmployeeToProject { get; set; }
public Division Div
{
get { return _div; }
set
{
if (_div != value)
{
_div = value;
PropertyChanged(this, new PropertyChangedEventArgs("Div"));
}
}
}
private void OnAddNewProject()
{
Div.AddNewProject();
}
private void OnAddEmployeeToProject(Project proj)
{
var availables = proj.AvailableEmployees;
if (availables.Count > 0)
{
proj.Members.Add(availables[0]);
}
}
}
However, I cannot get the combobox for each employee in each project to work. It seems like the selected item/value is bound to the itemssource, and each time the combobox turns out blank. I've tried to do this also with SelectedValue and SelectedItem properties for the combobox, but none worked.
How do I get these two separated. Is there anything else I'm missing here?
OK. After so many experiments the best solution I came up with was to create my own user control that is composed of both a button and a combobox that imitate the behavior I was expecting of the combobox on it own.
First, I had a really stupid mistake in the model where both lists of members Project and Division contain the same instances of Employee, which makes the AvailableEmployees property buggy. What I really needed to do is to create a list of copies of employees in the Project instead of just references.
In any case, I created a new user control and called it DynamicSourceComboBox. The XAML of this control looks like this:
<Grid>
<Button x:Name="selected"
Content="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=SelectedValue}"
Click="selected_Click"/>
<ComboBox x:Name="selections"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=ItemsSource}"
DisplayMemberPath="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=DisplayMemberPath}"
Visibility="Collapsed"
SelectionChanged="selections_SelectionChanged"
MouseLeave="selections_MouseLeave"/>
</Grid>
I have here a few bindings from the button and the combobox to properties in my user control. These are actually dependency properties. The code-behind of my user control looks like this:
public partial class DynamicSourceComboBox : UserControl
{
public DynamicSourceComboBox()
{
InitializeComponent();
}
public object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(DynamicSourceComboBox), new PropertyMetadata(null));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
ComboBox.ItemsSourceProperty.AddOwner(typeof(DynamicSourceComboBox));
public string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty =
ComboBox.DisplayMemberPathProperty.AddOwner(typeof(DynamicSourceComboBox));
private void selected_Click(object sender, RoutedEventArgs e)
{
selected.Visibility = Visibility.Hidden;
selections.Visibility = Visibility.Visible;
selections.IsDropDownOpen = true;
}
private void selections_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
if (e.AddedItems.Count == 1)
{
var item = e.AddedItems[0];
Type itemType = item.GetType();
var itemTypeProps = itemType.GetProperties();
var realValue = (from prop in itemTypeProps
where prop.Name == DisplayMemberPath
select prop.GetValue(selections.SelectedValue)).First();
SelectedValue = realValue;
}
}
private void selections_MouseLeave(object sender, MouseEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
}
}
These dependency properties imitate the properties with similar names in ComboBox but they are hooked up to the internal combobox and the button in a way that makes them behave together as a single complex combobox.
The Click event in the button hides it and present the combobox to make the effect of just a box that is opening. Then I have a SelectionChanged event in the combobox firing to update all the needed information and a MouseLeave event just in case the user doesn't make any real selection change.
When I need to use the new user control, I set it up like this:
<local:DynamicSourceComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorLevel=1, AncestorType=ListBox}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
SelectedValue="{Binding FirstName, Mode=TwoWay}"/>
Of course, for all of it to work, I have to make a lot of hookups with PropertyChanged events in the models, so the Projects instance will know to raise a PropertyChanged event for AvailableEmployees any time a change is made, but this is not really the concern of this user control itself.
This is a pretty clunky solution, with a lot of extra code that is a bit hard to follow, but it's really the best (actually only) solution I could have come up with to the problem I had.
I have created a simple form that inserts/updates/deletes a values for Northwind Customers.
Everything works fine, except in order to see a results, I have to close it, and reopen again.
My form looks like this :
I've searched tens of articles on how to refresh ListBox, but all of those use interface implementing, or using DataSets, and stuff I have never heard of and cannot implement. It's a very simple project, using simple procedures. Is there an easy way to refresh the list of customers without adding many lines of code?
The simple answer is: myListBox.Items.Refresh();
Are you using ObservableCollection and does your model implement INotifyPropertyChanged these two things will automaticly update the ListBox on any change. no need to explicitly refresh the list.
Here is a small example of using ObservableCollection and INotifyPropertyChanged, obviously you will populate your ObservableCollection from your SQL database.
Window:
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ObservableCollection<MyModel> _list = new ObservableCollection<MyModel>();
private MyModel _selectedModel;
public MainWindow()
{
InitializeComponent();
List.Add(new MyModel { Name = "James", CompanyName = "StackOverflow"});
List.Add(new MyModel { Name = "Adam", CompanyName = "StackOverflow" });
List.Add(new MyModel { Name = "Chris", CompanyName = "StackOverflow" });
List.Add(new MyModel { Name = "Steve", CompanyName = "StackOverflow" });
List.Add(new MyModel { Name = "Brent", CompanyName = "StackOverflow" });
}
public ObservableCollection<MyModel> List
{
get { return _list; }
set { _list = value; }
}
public MyModel SelectedModel
{
get { return _selectedModel; }
set { _selectedModel = value; NotifyPropertyChanged("SelectedModel"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
Xaml
<Window x:Class="WpfApplication11.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" Name="UI">
<Grid>
<ListBox ItemsSource="{Binding ElementName=UI, Path=List}" SelectedItem="{Binding ElementName=UI, Path=SelectedModel}" Margin="0,0,200,0" DisplayMemberPath="DisplayMember" SelectedIndex="0" />
<StackPanel HorizontalAlignment="Left" Height="100" Margin="322,10,0,0" VerticalAlignment="Top" Width="185">
<TextBlock Text="Name" />
<TextBox Height="23" TextWrapping="Wrap" Text="{Binding ElementName=UI, Path=SelectedModel.Name, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="Company Name" />
<TextBox Height="23" TextWrapping="Wrap" Text="{Binding ElementName=UI, Path=SelectedModel.CompanyName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
</Window>
Model
public class MyModel : INotifyPropertyChanged
{
private string _name;
private string _companyName;
public string Name
{
get { return _name; }
set { _name = value; NotifyPropertyChanged("Name"); }
}
public string CompanyName
{
get { return _companyName; }
set { _companyName = value; NotifyPropertyChanged("CompanyName"); }
}
public string DisplayMember
{
get { return string.Format("{0} ({1})", Name, CompanyName); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
PropertyChanged(this, new PropertyChangedEventArgs("DisplayMember"));
}
}
}
In this case any edit to properties will Update your list instantly, also will update when new Items are added/removed.
How about calling ListBox.UpdateLayout?
Of course you also need to update the particular item(s) so that it returns the updated string from the ToString method.
UPDATE: I think you also need to call ListBox.InvalidateArrange before you call ListBox.UpdateLayout.
Use INotifyPropertyChanged is the best way, refresh the entire list is not a good idea.
Main entrance:
public partial class MainWindow : Window
{
private BindingList<FoodModel> foodList = new BindingList<FoodModel>();
public MainWindow()
{
InitializeComponent();
}
private void Button1_Click(object sender, RoutedEventArgs e)
{
foodList.Add(new FoodModel { foodName = "apple1" });
foodList.Add(new FoodModel { foodName = "apple2" });
foodList.Add(new FoodModel { foodName = "apple3" });
FoodListBox.ItemsSource = foodList;
}
private void Button2_Click(object sender, RoutedEventArgs e)
{
foodList[0].foodName = "orange";
}
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
FoodListBox.Items.Refresh();
}
}
Model:
public class FoodModel: INotifyPropertyChanged
{
private string _foodName;
public string foodName
{
get { return _foodName; }
set
{
if (_foodName != value)
{
_foodName = value;
PropertyChanged(this, new PropertyChangedEventArgs("foodName"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
XAML:
<ListBox HorizontalAlignment="Center" Name="FoodListBox" VerticalAlignment="Top" Width="194" Height="150">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding foodName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>