Reputation: 59
I want combo boxes in my MVVM form to refresh both their selection box that shows the currently selected item and the drop-down list item of the currently selected item as soon as the bound data is changed. I can't get this to happen. It is important that the picture is refreshed also.
The example form has 2 combo boxes showing preloaded people and a button to add a person and a button to alter some data on an existing person. When that is clicked, the Person.Type
field is changed a little sort of at random and a different picture file string is stored in Person.PicFullPath
.
<Window x:Class="combobox_test__01.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:combobox_test__01"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="420" WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:VM x:Key="vm" />
<DataTemplate x:Key="cboTemplate" >
<StackPanel Orientation="Horizontal">
<Border Height="80" Width="80" >
<Image>
<Image.Source>
<PriorityBinding>
<Binding Path="PicFullPath" Mode="TwoWay"/>
</PriorityBinding>
</Image.Source>
</Image>
</Border>
<Canvas Height="80" Width="160" >
<StackPanel>
<TextBlock HorizontalAlignment="Left" Height="16" Text="{Binding Path=Name, Mode=TwoWay}" />
<TextBlock HorizontalAlignment="Left" Height="16" Text="{Binding Path=Type, Mode=TwoWay}" />
</StackPanel>
</Canvas>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource vm}" />
</Window.DataContext>
<Grid>
<ComboBox Name="cbo1" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" Margin="19,11,0,0" VerticalAlignment="Top" Width="159"
ItemsSource="{Binding People}"
SelectedItem="{Binding SelectedPerson}"
DisplayMemberPath="Type" />
<Button Content="New Row" HorizontalAlignment="Left" Margin="224,11,0,0" VerticalAlignment="Top" Width="142"
Command="{Binding NewRowCommand}" />
<Button Content="Change current row data" HorizontalAlignment="Left" Margin="224,48,0,0" VerticalAlignment="Top" Width="142"
Command="{Binding AlterRowCommand}" />
<ComboBox Name="cbo2" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" Margin="19,130,0,0" VerticalAlignment="Top" Width="159" Height="82"
ItemsSource="{Binding People}"
SelectedItem="{Binding SelectedPerson}"
ItemTemplate="{StaticResource cboTemplate}" />
</Grid>
</Window>
using myAssemblies;
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.ComponentModel;
namespace combobox_test__01
{
public class VM : INotifyPropertyChanged
{
private ObservableCollection<Person> people;
public ObservableCollection<Person> People
{
get { return people; }
set
{
people = value;
OnPropertyChanged("People");
}
}
private Person selectedPerson;
public Person SelectedPerson
{
get { return selectedPerson; }
set
{
selectedPerson = value;
OnPropertyChanged("SelectedPerson");
People = People;
}
}
private int idx = 0;
private string[,] newP;
private string DefaultPic = @"D:\Visual Studio Testing\Icons\user-icon.png",
NewPic = @"D:\Visual Studio Testing\Small size pictures\mugshot";
private Random random = new Random();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
public VM()
{
People = new ObservableCollection<Person>()
{
new Person() {Name="Aa", Type="Taa", PicFullPath=DefaultPic},
new Person() {Name="Bb", Type="Tbb", PicFullPath=DefaultPic},
new Person() {Name="Cc", Type="Tcc", PicFullPath=DefaultPic}
};
newP = new string[4, 3] { { "Dd", "Tdd", "" }, { "Ee", "Tee", "" }, { "Ff", "Tff", "" }, { "Gg", "Tgg", "" } };
}
public ICommand AlterRowCommand => new RelayCommandBase(AlterRow);
private void AlterRow(object parameter)
{
//Make SelectedPerson.Type into a string ending with 2 randomish digits
string fmts, picChoice;
GetDifferentData(out fmts, out picChoice);
string s = SelectedPerson.Type.Substring(0,3).PadRight(5);
SelectedPerson.Type = s + fmts;
// Use those 2 randomish digits to choose a picture from existing ones
SelectedPerson.PicFullPath = NewPic + picChoice;
// Force both setters to execute
SelectedPerson = SelectedPerson;
}
public ICommand NewRowCommand => new RelayCommandBase(NewRow);
private void NewRow(object parameter)
{
string fmts, picChoice;
GetDifferentData(out fmts, out picChoice);
People.Add(new Person() { Name = newP[idx, 0], Type = newP[idx++, 1], PicFullPath = NewPic + picChoice });
}
private void GetDifferentData(out string fmts, out string picChoice)
{
int randomi = random.Next(1, 35);
fmts = randomi.ToString("D2");
picChoice = fmts + ".jpg";
}
}
public class Person
{
public string Name { get; set; }
public string Type { get; set; }
public string PicFullPath { get; set; }
}
}
This is the behaviour:
Run the application
The view model is in State 1
Click 'Change current row data'
The view model is in State 2
no visible change: I want the selection boxes to show the altered Type
data and the different picture
Click combo box cbo1
the drop-down list shows the change of People.Type
. The selection box still shows the old data.
Click combo box cbo2
the drop-down list shows the change of People.Type
and the different picture. The selection box still shows the old data.
Click 'Change current row data' again without moving the selected item.
The view model is in State 3.
no visible change: with the drop-down list retracted the form looks the same as on initialization.
Click combo box cbo1
the drop-down list still shows the first change of People.Type
, but People.Type
has been changed again. The selection box still shows the original data.
Click combo box cbo2
the drop-down list has not altered since last time it dropped. The selection box still shows the original data.
There have been 3 changes of state in the view model but the combo boxes are showing State 1 in the selection box and State 2 in the drop-down list. I want them to show State 3 in both places.
Click combo box cbo1 and select another item, then select the first item. So we have moved the selected item and moved it back.
both combo boxes show the latest data in the selection box
the drop-down lists are out of date. Both show State 2, so changing the selection and changing it back has not changed the displayed data in the drop-down list.
Click 'New Row'
The view model is in State 4
no visible change: as expected - the new person is not selected so there wouldn't be a visible change.
Click the combo boxes
The new person displays in the drop-down list but the first item still shows State 2.
No clicking will make the drop-down lists show changes after the first change - they are stuck showing State 2.
How do I make the selection boxes and drop-down lists always show the latest data?
Upvotes: 0
Views: 917
Reputation: 59
I have a solution. It's not very clean but it's simple and it works. This is the updated view model code with a few small changes and a new method:
using npAssemblies;
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.ComponentModel;
namespace combobox_test__01
{
public class VM : INotifyPropertyChanged
{
private ObservableCollection<Person> people;
public ObservableCollection<Person> People
{
get { return people; }
set
{
people = value;
OnPropertyChanged("People");
}
}
private Person selectedPerson;
public Person SelectedPerson
{
get { return selectedPerson; }
set
{
selectedPerson = value;
OnPropertyChanged("SelectedPerson");
}
}
private int cboPeopleSelectedIndex;
public int CboPeopleSelectedIndex
{
get { return cboPeopleSelectedIndex; }
set
{
cboPeopleSelectedIndex = value;
OnPropertyChanged("CboPeopleSelectedIndex");
}
}
private int newPidx = 0;
private string[,] newP;
private string DefaultPic = @"D:\Visual Studio Testing\Icons\user-icon.png",
NewPic = @"D:\Visual Studio Testing\Small size pictures\mugshot";
private Random random = new Random();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
public VM()
{
People = new ObservableCollection<Person>()
{
new Person() {Name="Aa", Type="Taa", PicFullPath=DefaultPic},
new Person() {Name="Bb", Type="Tbb", PicFullPath=DefaultPic},
new Person() {Name="Cc", Type="Tcc", PicFullPath=DefaultPic}
};
newP = new string[4, 3] { { "Dd", "Tdd", "" }, { "Ee", "Tee", "" }, { "Ff", "Tff", "" }, { "Gg", "Tgg", "" } };
}
public ICommand AlterRowCommand => new RelayCommandBase(AlterRow);
private void AlterRow(object parameter)
{
//Make SelectedPerson.Type into a string ending with 2 randomish digits
string fmts, picChoice;
GetDifferentData(out fmts, out picChoice);
string s = SelectedPerson.Type.Substring(0,3).PadRight(5);
SelectedPerson.Type = s + fmts;
// Use those 2 randomish digits to choose a picture from existing ones
SelectedPerson.PicFullPath = NewPic + picChoice;
// refresh the control the collection is bound to
ResetBoundItems(SelectedPerson);
}
public ICommand NewRowCommand => new RelayCommandBase(NewRow);
private void NewRow(object parameter)
{
string fmts, picChoice;
int highIdx = People.Count;
GetDifferentData(out fmts, out picChoice);
Person newPerson = new Person() { Name = newP[newPidx, 0], Type = newP[newPidx++, 1], PicFullPath = NewPic + picChoice };
People.Add(newPerson);
// refresh the control the collection is bound to and select the new row
ResetBoundItems(newPerson, highIdx);
}
private void GetDifferentData(out string fmts, out string picChoice)
{
int randomi = random.Next(1, 35);
fmts = randomi.ToString("D2");
picChoice = fmts + ".jpg";
}
private void ResetBoundItems(Person inPerson, int gotoIndex = -1)
{
// refreshes the display of the combobox otherwise it is visually stale
int idx = gotoIndex;
if (gotoIndex == -1)
{
// a preferred index was not supplied; use the currently selected index
idx = CboPeopleSelectedIndex;
}
// save a copy of the current selected person
Person tmpP = (Person)inPerson.Clone();
// save the current ItemsSource
ObservableCollection<Person> refPC = new ObservableCollection<Person>();
refPC = People;
// reset the ItemsSource
ObservableCollection<Person> tmpPC = new ObservableCollection<Person>();
People = tmpPC;
// set it back
People = refPC;
// restore the selected person
SelectedPerson = (Person)tmpP.Clone();
// select the relevant row
CboPeopleSelectedIndex = idx;
}
}
public class Person
{
public string Name { get; set; }
public string Type { get; set; }
public string PicFullPath { get; set; }
public Person Clone()
{
return (Person)this.MemberwiseClone();
}
}
}
The view needs the ComboBoxes to have a bound SelectedIndex so add
SelectedIndex="{Binding CboPeopleSelectedIndex}"
to the ComboBoxes definition.
The new method ResetBoundItems()
handles the refreshing. The Person class needs a Clone()
method. I made the ComboBox select the new row when adding. This solution will work for ListBoxes too.
Upvotes: 0