MVVM Editable ComboBox without changing the SelectedItem - c#

I have exactly the same requirements as this user: MVVM Editable ComboBox Bindings.
I've tried the accepted answer:
"Bind a property like "EditedServerName" to Combobox.Text. When the
"EditedServerName" is changed you can set the value to the
"ServerName" of your SelectedServer."
But it's not working because when I try to intercept "EditedServerName" the "SelectedServer" is null. I believe it's because the control tries to search the "ServerName" that is being edited, in the collection, and obviously fails to retrieve an element. This is very clear when I start editing and the textblock with the "ServerID" just gets immediately empty.
XAML:
<ComboBox IsEditable= "True"
ItemsSource= "{Binding Servers}"
DisplayMemberPath= "ServerName"
SelectedItem="{Binding SelectedServer}"
Text= "{Binding EditedServerName, UpdateSourceTrigger=LostFocus}" />
<TextBlock Text="{Binding SelectedServer.ServerID}"/>
ViewModel:
public List<Server> Servers { get; set; }
public Server SelectedServer { get; set; }
private string editedServerName;
public string EditedServerName
{
get { return editedServerName; }
set
{
editedServerName = value;
SelectedServer.ServerName = value;
}
}
public MainViewModel()
{
Servers = new List<Server>();
Servers.Add(new Server { ServerID = 0, ServerName = "Local" });
Servers.Add(new Server { ServerID = 1, ServerName = "Remote" });
}
I know I could temporarily store the "SelectedServer" on another object, but I would like a better turn-around if possible.

A couple of things:
Your MainViewModel is not making proper use of the INotifyPropertyChanged interface, which is recommended for data binding back to the view. This is the reason that there's no change to the UI with your currently posted code.
You're correct in saying that once the EditedServerName is changed, the SelectedServer will become null, but with point 1, it's getting to that point either. Even if we fix the code to accommodate data binding, the SelectedServer property is not handling null.
With that in mind, here's what the code might look like if we modify the code to accommodate the above points:
Below code helps notify the view of any changes to our Models / ViewModels
public class BaseNotifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Here's what our updated model looks like. Note the use of OnPropertyChanged which is defined in BaseNotifier, as this will update the View with the new server name
public class Server : BaseNotifier
{
private string _serverName;
public string ServerName
{
get { return _serverName; }
set
{
_serverName = value;
OnPropertyChanged();
}
}
private int _serverID;
public int ServerID
{
get { return _serverID; }
set
{
_serverID = value;
OnPropertyChanged();
}
}
}
This is what the ViewModel now looks like. For ViewModels, use ObservableCollection instead of List to allow autmatic view updates when inserting data. You'll also notice that the code checks for null in both the SelectedServer and EditedServerName which should handle your original question.
public class MainViewModel : BaseNotifier
{
private string editedServerName;
private Server selectedServer = null;
public ObservableCollection<Server> Servers { get; set; }
public Server SelectedServer
{
get { return selectedServer; }
set
{
if (value != null)
{
selectedServer = value;
OnPropertyChanged();
}
}
}
public string EditedServerName
{
get { return editedServerName; }
set
{
editedServerName = value;
if (SelectedServer != null)
{
SelectedServer.ServerName = value;
}
OnPropertyChanged();
}
}
public MainViewModel()
{
Servers = new ObservableCollection<Server>();
Servers.Add(new Server { ServerID = 0, ServerName = "Local" });
Servers.Add(new Server { ServerID = 1, ServerName = "Remote" });
}
}

Related

Trying to Filter a bound ObservableCollection to a combobox based on another ComboBox Value not working

I see several other posts about this but I cannot seem to understand exactly how to get this working properly for my usage.
Here is what I have in a nutshell.
I have two Comboboxes--Role and Position.
I have both of these bound to an ObservableCollection which has Enum Values Converted to strings loaded into it on instantiation.
<ComboBox x:Name="empRoleCB" ItemsSource="{Binding Role}" SelectedItem="{Binding RoleStr}"/>
<ComboBox x:Name="empPositionCB" ItemsSource="{Binding Pos}" SelectedItem="{Binding PosStr}"/>
In my ViewModel:
public abstract class EmployeeMenuVMBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if(!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
}
class EmployeeMenuVM : EmployeeMenuVMBase
{
private ObservableCollection<string> _pos = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Positions)));
private ObservableCollection<string> _role = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Roles)));
public ObservableCollection<string> Pos { get => _pos; }
public ObservableCollection<string> Role { get => _role; }
public string RoleStr
{
get => _roleStr;
set => SetProperty(ref _roleStr, value);
}
public string PosStr
{
get => _posStr;
set => SetProperty(ref _posStr, value);
}
}
What I want to happen is when a Role is selected, based on that selection, only certain Positions should be shown. For instance if I select "Customer Service" as a Role, then Position should only contain "Manager", "CSS" and "None". If Role is "Admin" then Position should only contain "None", and so on and so forth.
The struggle I have is how to filter this properly. I see something with using CollectionViewSource but I am unsure how to get this to work with my example.
I have 5 roles and each role will have a different list of positions that need to be shown.
What is the best way to make this work with MINIMAL extra code or XAML?
One of the things I really dislike about WPF is seemingly simple things need huge amounts of code to make them work properly many times.
First, if you think that WPF is complicated. So, you are using it wrongly.
I suggest you to use the Filter of CollectionViewSource as flow:
<ComboBox x:Name="empPositionCB" ItemsSource="{Binding MyPositionFilter}" SelectionChanged="RoleComboBox_SelectionChanged" ....../>
public ICollectionView MyPositionFilter { get; set; }
//ctor
public MyUserControlOrWindow()
{
//Before InitComponent()
this.MyPositionFilter = new CollectionViewSource { Source = MyPosObservableCollection }.View;
InitComponent();
}
public void RoleComboBox_SelectionChanged(object sender,EventArgs e)
{
//Get the selected Role (the ? is to prevent NullException (VS 2015 >))
Role r = empRoleCB.SelectedItem as Role;
//Apply the filter
this.MyPositionFilter.Filter = item =>
{
//Make you sure to convert correcteley your Enumeration, I used it here like a class
Position p = item as Position;
//Put your condition here. For example:
return r.ToLowers().Contains(p.ToLower());
//Or
return (r != null && r.Length >= p.Length);
};
}
The filter does not change your Collection, All hidden item stay in your ObservableCollection.
This can all be done in your ViewModel by changing the value of the Positions (Pos) observable collection when the role changes.
class EmployeeMenuVM : EmployeeMenuVMBase
{
public EmployeeMenuVM()
{
var emptyPositions = new List<Global.Positions>()
{ Global.Positions.None };
_rolePositions.Add(Global.Roles.None, emptyPositions);
var customerServicePositions = new List<Global.Positions>()
{ Global.Positions.None, Global.Positions.CSS, Global.Positions.Manager };
_rolePositions.Add(Global.Roles.CustomerService, customerServicePositions);
}
private Dictionary<Global.Roles, List<Global.Positions>> _rolePositions = new Dictionary<Global.Roles, List<Global.Positions>>();
private string _roleStr;
private string _posStr;
private ObservableCollection<string> _pos = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Positions)));
private ObservableCollection<string> _role = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Roles)));
public ObservableCollection<string> Pos
{
get => _pos;
set
{
SetProperty(ref _pos, value);
}
}
public ObservableCollection<string> Role
{
get => _role;
}
public string RoleStr
{
get => _roleStr;
set
{
if (SetProperty(ref _roleStr, value))
{
Global.Roles role = (Global.Roles)Enum.Parse(typeof(Global.Roles), value);
var positions = _rolePositions[role].Select(p => p.ToString());
Pos = new ObservableCollection<string>(positions);
}
}
}
public string PosStr
{
get => _posStr;
set => SetProperty(ref _posStr, value);
}
}
Here is a working tester code just to see the main idea of how to do the filtering:
MainWindow.xaml
<Window x:Class="WpfApplication3.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:WpfApplication3"
x:Name="ThisView"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="600">
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=Roles, ElementName=ThisView}"
SelectedItem="{Binding Path=SelectedRole, ElementName=ThisView}"
Width="300" Height="60"/>
<ComboBox ItemsSource="{Binding Path=PositionCollectionView, ElementName=ThisView}" Width="300" Height="60"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window, INotifyPropertyChanged
{
public ICollectionView PositionCollectionView { get; set; }
public ObservableCollection<string> Roles { get; set; } = new ObservableCollection<string>();
public ObservableCollection<string> Positions { get; set; } = new ObservableCollection<string>();
private string _selectedRole = String.Empty;
public string SelectedRole
{
get { return _selectedRole; }
set
{
_selectedRole = value;
OnPropertyChanged();
//This Refresh activates the Filter again, so that every time you select a role, this property will call it.
PositionCollectionView.Refresh();
}
}
public MainWindow()
{
PositionCollectionView = CollectionViewSource.GetDefaultView(Positions);
PositionCollectionView.Filter = PositionsFilter;
//use you enums here
Roles.Add("Role1");
Roles.Add("Role2");
Roles.Add("Role3");
Roles.Add("Role4");
Positions.Add("Position1");
Positions.Add("Position2");
Positions.Add("Position3");
Positions.Add("Position4");
InitializeComponent();
}
private bool PositionsFilter(object position)
{
bool result = true;
//place your code according to the Role selected to decide wheather "position" should be in the position list or not
return result;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Hope it helps..

Why is this ListView not changing with changes to the bound property?

Basic question from a novice. I've been stuck on this and have read through a lot of material and several similar questions on SO; hopefully not a completely duplicate question. I simplified the code as much as I know how to.
I'm trying to make the ListView show a filtered ObservableCollection) property (as the ItemsSource?), based on the selection in the ComboBox.
Specifically, which "meetings" have this "coordinator" related to it.
I'm not seeing any data errors in the output while it's running and debugging shows the properties updating correctly, but the ListView stays blank. I'm trying to avoid any code-behind on the View, there is none currently.
Thanks!
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<Meeting> meetings;
public ObservableCollection<Meeting> Meetings
{
get
{
return meetings;
}
set
{
meetings = value;
OnPropertyChanged("ListProperty");
OnPropertyChanged("Meetings");
}
}
private string coordinatorSelected;
public string CoordinatorSelected
{
get
{
return coordinatorSelected;
}
set
{
coordinatorSelected = value;
Meetings = fakeDB.Where(v => v.CoordinatorName == CoordinatorSelected) as ObservableCollection<Meeting>;
}
}
private ObservableCollection<string> comboProperty = new ObservableCollection<string> { "Joe", "Helen", "Sven" };
public ObservableCollection<string> ComboProperty
{
get
{
return comboProperty;
}
}
private ObservableCollection<Meeting> fakeDB = new ObservableCollection<Meeting>() { new Meeting("Joe", "Atlas"), new Meeting("Sven", "Contoso"), new Meeting("Helen", "Acme") };
public ObservableCollection<Meeting> ListProperty
{
get
{
return Meetings;
}
}
public class Meeting
{
public string CoordinatorName { get; set; }
public string ClientName { get; set; }
public Meeting(string coordinatorName, string clientName)
{
CoordinatorName = coordinatorName;
ClientName = clientName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<Window.Resources>
<local:ViewModel x:Key="VM"></local:ViewModel>
</Window.Resources>
<DockPanel DataContext="{StaticResource ResourceKey=VM}">
<ComboBox Margin="10" ItemsSource="{Binding ComboProperty}" SelectedItem="{Binding CoordinatorSelected}" DockPanel.Dock="Top"/>
<ListView Margin="10" ItemsSource="{Binding ListProperty, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="ClientName"/>
</DockPanel>
Update:
This seems to show that the lambda is returning a Meeting object but the assignment to Meetings is failing. Is this an error in casting maybe?
Thanks again.
You always have to change a property's backing field before you fire a PropertyChanged event. Otherwise a consumer of the event would still get the old value when it reads the property.
Change the Meetings property setter like this:
public ObservableCollection<Meeting> Meetings
{
get
{
return meetings;
}
set
{
meetings = value;
OnPropertyChanged("ListProperty");
OnPropertyChanged("Meetings");
}
}
I believe I've found two solutions to the same problem. The error pointed out #Clemens was also part of the solution. The Meetings property problem is solved if I change ListProperty and Meetings to IEnumerable. Alternatively this approach without changing the type, which I believe invokes the collection's constructor with the filtered sequence as an argument.
set
{
coordinatorSelected = value;
var filteredList = fakeDB.Where(v => v.CoordinatorName == coordinatorSelected);
Meetings = new ObservableCollection<Meeting>(filteredList);
OnPropertyChanged("ListProperty");
}

Binding Radio Button IsChecked to object's current array of element's state

I am developing a small utility using C#/WPF/MVVM which would allow to set the input state of a controller we are using for testing. The communication between the app I am developing and the hardware/our web service communication to the hardware is only one way, meaning that the app will only be able to set the state of the inputs, but not get the states.
Another point to mention is that some types are already defined for this in some other parts of our solution, which are all in F#. To do my app, I am currently using C#. So I did a Unit class to wrap around the LocalControllerTypes.LocalController type defined in F#, containing a lot of needed information.
In order to do that, I have an enum enumerating the InputState possible (currently there is Active or Normal, but that list could potentially grow with time). Also, the number of inputs present on each unit type is different (some have 2, some have 4, some have more), so I have an ItemControl binded on the selected unit's array of Inputs, which unfortunately only contains the Name of the input which I have to display. The unit has 2 other properties related to the inputs it has, InputWriters, which is an array of a type which is used to send the command to the hardware/web service communicating with that hardware, and InputStates, which is an array of InputState for each input it has, as last set in the app (since we can't get the state from the hardware).
Now I would like to bind the IsChecked property of the radio buttons (which is what I define as ItemTemplate of the ItemsControl) to the InputState of the currently SelectedUnit (in my ViewModel). The problem I am having, is that I would somehow need to know the radio button is for which index of the SelectedUnit's Inputs array, in order to get the item at the same index for the SelectedUnit's InputStates property.
Is there any way to achieve this?
MainWindow.xaml:
...
<ItemsControl Grid.Row="1" ItemsSource="{Binding SelectedUnit.LocalControllerInfo.Inputs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10" FontSize="15" Style="{StaticResource TextBlockNormalBase}" Text="{Binding InputName}"/>
<StackPanel Orientation="Horizontal">
<RadioButton Margin="10" Foreground="White" Content="Normal"
IsChecked="{Binding Path=?,
Converter={StaticResource inputToBoolConverter},
ConverterParameter=?}"/>
<RadioButton Margin="10" Foreground="White" Content="Active"
IsChecked="{Binding Path=?,
Converter={StaticResource inputToBoolConverter},
ConverterParameter=?}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
...
Unit.cs:
public class Unit : BindableObject
{
public enum InputState
{
Normal,
Active
}
private LocalControllerTypes.LocalController _localControllerInfo;
private LocalControllerTypes.ArduinoInjector[] _arduinoInjector;
private WebWriter.WebWriter[] _inputWriters;
private SNMPNetworkSwitchConnection.SNMPNetworkSwitchConnection _networkSwitchConnection;
private InputState[] _inputStates;
private bool _isUnitConnected;
public Unit(LocalControllerTypes.LocalController localControllerInfo,
LocalControllerTypes.ArduinoInjector[] arduinoInjector,
WebWriter.WebWriter[] inputWriters,
SNMPNetworkSwitchConnection.SNMPNetworkSwitchConnection networkSwitchConnection)
{
_localControllerInfo = localControllerInfo;
_arduinoInjector = arduinoInjector;
_inputWriters = inputWriters;
_networkSwitchConnection = networkSwitchConnection;
// This assumption might not always be true, but there is no way for now to get the input state
_inputStates = Enumerable.Repeat(InputState.Normal, _inputWriters.Length).ToArray();
// This assumption might not always be true, but there is no way for now to get the connection state
_isUnitConnected = true;
}
public LocalControllerTypes.LocalController LocalControllerInfo
{
get
{
return _localControllerInfo;
}
set
{
if (_localControllerInfo != value)
{
_localControllerInfo = value;
RaisePropertyChanged();
}
}
}
public LocalControllerTypes.ArduinoInjector[] ArduinoInjectors
{
get
{
return _arduinoInjector;
}
set
{
if (_arduinoInjector != value)
{
_arduinoInjector = value;
RaisePropertyChanged();
}
}
}
public WebWriter.WebWriter[] InputWriters
{
get
{
return _inputWriters;
}
set
{
if (_inputWriters != value)
{
_inputWriters = value;
RaisePropertyChanged();
}
}
}
public SNMPNetworkSwitchConnection.SNMPNetworkSwitchConnection NetworkSwitchConnection
{
get
{
return _networkSwitchConnection;
}
set
{
if (_networkSwitchConnection != value)
{
_networkSwitchConnection = value;
RaisePropertyChanged();
}
}
}
public InputState[] InputStates
{
get
{
return _inputStates;
}
set
{
if (_inputStates != value)
{
_inputStates = value;
RaisePropertyChanged();
}
}
}
public bool IsUnitConnected
{
get
{
return _isUnitConnected;
}
set
{
if (_isUnitConnected != value)
{
_isUnitConnected = value;
RaisePropertyChanged();
}
}
}
}
MainViewModel.cs:
public class MainViewModel : INotifyPropertyChanged
{
private Unit _selectedUnit;
private ObservableCollection<Unit> _units;
private string _reader1RawCardData;
private string _reader2RawCardData;
private int _reader1BitsCount;
private int _reader2BitsCount;
public event PropertyChangedEventHandler PropertyChanged;
public MainViewModel(IUnitStore unitStore)
{
UnitStore = unitStore;
// We could use directly the unitstore instead of creating another container and binding on that, but
// not doing so will allow us to add unit filtering further down the road
_units = new ObservableCollection<Unit>(unitStore.Units);
_selectedUnit = _units.First();
_reader1RawCardData = "";
_reader2RawCardData = "";
_reader1BitsCount = 0;
_reader2BitsCount = 0;
}
protected void RaisePropertyChanged([CallerMemberName]string propertName = "")
{
var temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs(propertName));
}
}
protected void RefreshUnitStore(object obj)
{
UnitStore.UpdateStore();
Units = new ObservableCollection<Unit>(UnitStore.Units);
SelectedUnit = Units.First();
}
protected void SendReaderCardSwipe(object obj)
{
int unitReaderNumber = (int)obj;
IPAddress arduinoIp = SelectedUnit.LocalControllerInfo.Readers[unitReaderNumber - 1].InjectorIp;
int injectorNumber = SelectedUnit.LocalControllerInfo.Readers[unitReaderNumber - 1].InjectorNumber;
string serviceUrl = SelectedUnit.ArduinoInjectors.Where(injector => injector.Ip.Equals(arduinoIp)).First().Url;
InjectorInterface.CardSwipe<IPAddress>(serviceUrl, arduinoIp, injectorNumber, Reader1BitsCount, Reader1RawCardData);
}
protected void UpdateSelectedUnitConnectionState(object obj)
{
((INetworkConnection.INetworkConnection)SelectedUnit.NetworkSwitchConnection).SetConnection(SelectedUnit.IsUnitConnected);
}
public IUnitStore UnitStore
{
get;
private set;
}
public Unit SelectedUnit
{
get
{
return _selectedUnit;
}
set
{
if (_selectedUnit != value)
{
_selectedUnit = value;
RaisePropertyChanged();
}
}
}
public ObservableCollection<Unit> Units
{
get
{
return _units;
}
set
{
if (_units != value)
{
_units = value;
RaisePropertyChanged();
}
}
}
public string Reader1RawCardData
{
get
{
return _reader1RawCardData;
}
set
{
if (_reader1RawCardData != value)
{
_reader1RawCardData = value;
RaisePropertyChanged();
}
}
}
public string Reader2RawCardData
{
get
{
return _reader2RawCardData;
}
set
{
if (_reader2RawCardData != value)
{
_reader2RawCardData = value;
RaisePropertyChanged();
}
}
}
public int Reader1BitsCount
{
get
{
return _reader1BitsCount;
}
set
{
if (_reader1BitsCount != value)
{
_reader1BitsCount = value;
RaisePropertyChanged();
}
}
}
public int Reader2BitsCount
{
get
{
return _reader2BitsCount;
}
set
{
if (_reader2BitsCount != value)
{
_reader2BitsCount = value;
RaisePropertyChanged();
}
}
}
public ICommand RefreshSourceCommand
{
get
{
return new RelayCommand(RefreshUnitStore);
}
}
public ICommand SendReaderCardSwipeCommand
{
get
{
return new RelayCommand(SendReaderCardSwipe);
}
}
public ICommand UpdateSelectedUnitConnectionStateCommand
{
get
{
return new RelayCommand(UpdateSelectedUnitConnectionState);
}
}
}
Your ItemsControl is bound to SelectedUnit.LocalControllerInfo.Inputs. What is the type of .Inputs?
As written your bindings will not have access to InputState or InputName. That's not really in the scope of "how to identify what array item goes with what enum"
To address your original issue, one possibility would be to nest some tuples and bind to that, a la
List<Tuple<int,State>> States = new List<Tuple<int,State>>();
States.Add(new Tuple<int, State>(1,State.Bar));
States.Add(new Tuple<int, State>(2, State.Foo));
States.Add(new Tuple<int, State>(3, State.Bar));

Databound Listview not refreshing when set up with Parallel.Invoke

I have a Window with a TabControl in it. The TabControl contains 5 different TabItems. Each TabItem has its own ViewModel associated as its DataContext, while the Window has a DataContext that has all 5 TabItem's view models as properties. The problem I am having is setup. There is a noticeable lag when I launch the Window (from my MainWindow) and I have spent a good chunk of time refactoring my code and making it faster by running things in parallel, making fewer calls to the database and running Tasks on semi-expensive operations. Everything works great, except for one TabItem and its view model. For some reason, the view does not refresh itself properly.
For instance, I have a view model called DiaryDescriptionViewModel that takes in List<SectionViewModel> and does stuff with it, with the view bound to a result collection. It works fine. My troublesome view model is called DiaryPayItemEditorViewModel and it too takes in a List<SectionViewModel> and does stuff with it, with the view bound to a result collection. Neither view models perform the work on List<SectionViewModel> on worker threads or anything. However both view models are instanced and set up in parallel, which I wouldn't think be the root of the problem.
In my DiaryPayItemEditorViewModel, I have a ObservableCollection<DiaryPayItemDetailViewModel> that a ListView is data bound to. The ListView never displays the data, even though it exists. If I take all of my view model initialization code out of a Parallel.Invoke call, then it binds and displays the data.
My assumption here is that the view is initialized (this.InitializeComponents) before the DiaryPayItemEditorViewModel is fully set up, which should be fine. Since my view models all implement INotifyPropertyChanged, the view should be notified that changes have taken place. For the life of me, I can't figure this out.
The following is the applicable source for the view Window view model (DiaryEditorViewModel), the view model who uses the same collection and works with binding (DiaryDescriptionViewModel and its child DiaryDescriptionDetailsViewModel) and then my troublesome view model (DiaryPayItemEditorViewModel and its child DiaryPayItemDetailViewModel).
DiaryEditorViewModel.cs
public class DiaryEditorViewModel : BaseChangeNotify
{
private DiaryViewModel diary;
private Project project;
private DiaryDetailsViewModel diaryDetailsViewModel;
private DiaryDescriptionViewModel diaryDescriptionViewModel;
private DiaryPayItemEditorViewModel diaryPayItemsViewModel;
private DiaryEquipmentEditorViewModel diaryEquipmentEditorViewModel;
private DiaryLaborViewModel diaryLaborViewModel;
// This is the designated constructor used by the app.
public DiaryEditorViewModel(Project project, Diary diary, UserViewModel user)
: base(user)
{
// Instance a new diary view model using the provided diary.
this.diary = new DiaryViewModel(diary, user);
this.project = project;
// Setup the repositories we will use.
var repository = new ProjectRepository();
var contractorRepository = new ContractorRepository();
// Setup the temporary collections used by the repositories.
var contractors = new List<Contractor>();
var contractorViewModels = new List<ContractorViewModel>();
var projectSections = new List<Section>();
var bidItemCollection = new List<BidItem>();
var subItemCollection = new List<SubItem>();
var sectionViewModels = new List<SectionViewModel>();
var equipmentCategories = new List<EquipmentCategory>();
var equipmentFuelTypes = new List<EquipmentFuelType>();
var equipmentList = new List<Equipment>();
var equipmentViewModels = new List<EquipmentViewModel>();
Task.Run(() =>
{
Parallel.Invoke(
// Fetch contractors for selected project.
() =>
{
contractors.AddRange(contractorRepository.GetContractorsByProjectId(diary.ProjectId));
equipmentCategories.AddRange(contractorRepository.GetEquipmentCategories());
equipmentFuelTypes.AddRange(contractorRepository.GetEquipmentFuelTypes());
equipmentList.AddRange(contractorRepository.GetEquipmentByProjectId(this.Project.ProjectId));
// Reconstruct the contractor->Equipment->FuelType & Category relationship.
contractorViewModels.AddRange(
contractors.Select(contractor =>
new ContractorViewModel(
contractor,
equipmentList.Where(equipment =>
equipment.ContractorId == contractor.ContractorId).Select(e =>
new EquipmentViewModel(
e,
contractor,
equipmentCategories.FirstOrDefault(cat =>
cat.EquipmentCategoryId == e.EquipmentCategoryId),
equipmentFuelTypes.FirstOrDefault(f =>
f.EquipmentFuelTypeId == e.EquipmentFuelTypeId))))));
},
() =>
{
// Fetch all of the Sections, Bid-Items and Sub-items for the project
projectSections.AddRange(repository.GetSectionsByProjectId(project.ProjectId));
bidItemCollection.AddRange(repository.GetBidItemsByProjectId(project.ProjectId));
subItemCollection.AddRange(repository.GetSubItemsByProjectId(project.ProjectId));
// Reconstruct the Section->BidItem->SubItem hierarchy.
sectionViewModels.AddRange(
projectSections.Select(s =>
new SectionViewModel(project, s,
bidItemCollection.Where(b => b.SectionId == s.SectionId).Select(b =>
new BidItemViewModel(project, b,
subItemCollection.Where(si => si.BidItemId == b.BidItemId))))));
}
);
// Once the parallel invocations are completed, instance all of the children view models
// using the view model collections we just set up.
Parallel.Invoke(
// Fetch contractors for selected project.
() =>
this.DiaryDetailsViewModel = new DiaryDetailsViewModel(
project,
diary,
user),
() => // This view model works just fine, with same constructor signature.
this.DiaryDescriptionViewModel = new DiaryDescriptionViewModel(
project,
diary,
user,
sectionViewModels),
() =>
this.DiaryPayItemEditorViewModel = new DiaryPayItemEditorViewModel(
project,
diary,
user,
sectionViewModels),
() => // This view model does not notify the UI of changes to its collection.
this.DiaryEquipmentEditorViewModel = new DiaryEquipmentEditorViewModel(
project,
diary,
user,
contractorViewModels),
() =>
// For the Labor view, we just pass the Contractor model collection rather than the view model collection
// since the Labor view does not need any of the additional equipment information.
this.DiaryLaborViewModel = new DiaryLaborViewModel(
project,
diary,
user,
contractors));
});
}
public Project Project
{
get
{
return this.project;
}
set
{
this.project = value;
this.OnPropertyChanged();
}
}
public DiaryViewModel Diary
{
get
{
return this.diary;
}
set
{
this.diary = value;
this.OnPropertyChanged();
}
}
public DiaryDetailsViewModel DiaryDetailsViewModel
{
get
{
return this.diaryDetailsViewModel;
}
set
{
this.diaryDetailsViewModel = value;
this.OnPropertyChanged();
}
}
public DiaryDescriptionViewModel DiaryDescriptionViewModel
{
get
{
return this.diaryDescriptionViewModel;
}
set
{
this.diaryDescriptionViewModel = value;
this.OnPropertyChanged();
}
}
public DiaryPayItemEditorViewModel DiaryPayItemEditorViewModel
{
get
{
return this.diaryPayItemsViewModel;
}
set
{
this.diaryPayItemsViewModel = value;
this.OnPropertyChanged();
}
}
public DiaryLaborViewModel DiaryLaborViewModel
{
get
{
return this.diaryLaborViewModel;
}
set
{
this.diaryLaborViewModel = value;
this.OnPropertyChanged();
}
}
public DiaryEquipmentEditorViewModel DiaryEquipmentEditorViewModel
{
get
{
return this.diaryEquipmentEditorViewModel;
}
set
{
this.diaryEquipmentEditorViewModel = value;
this.OnPropertyChanged();
}
}
}
DiaryDescriptionViewModel
This view model works just fine, with its this.DiaryDescriptions collection being bound to properly and displayed in the ListView
public class DiaryDescriptionViewModel : BaseDiaryViewModel, IDataErrorInfo
{
private ObservableCollection<DiaryDescriptionDetailsViewModel> diaryDescriptions;
private DiaryDescriptionDetailsViewModel selectedDiaryDescription;
public DiaryDescriptionViewModel()
{
}
public DiaryDescriptionViewModel(Project project, Diary diary, UserViewModel user, List<SectionViewModel> sections)
: base(project, diary, user)
{
// Restore any previously saved descriptions.
var diaryRepository = new DiaryRepository();
List<DiaryDescription> descriptions = diaryRepository.GetDiaryDescriptionsByDiaryId(diary.DiaryId);
this.ProjectSections = sections;
// Reconstruct our descriptions
this.diaryDescriptions = new ObservableCollection<DiaryDescriptionDetailsViewModel>();
foreach (DiaryDescription description in descriptions)
{
SectionViewModel section = this.GetSectionContainingBidItemId(description.BidItemId);
BidItemViewModel bidItem = section.GetBidItem(description.BidItemId);
var details = new DiaryDescriptionDetailsViewModel(description, section, bidItem);
details.PropertyChanged += ChildViewModelPropertyChanged;
this.diaryDescriptions.Add(details);
}
this.diaryDescriptions.CollectionChanged += this.DiaryDescriptionsOnCollectionChanged;
this.IsDirty = false;
}
public ObservableCollection<DiaryDescriptionDetailsViewModel> DiaryDescriptions
{
get
{
return this.diaryDescriptions;
}
set
{
if (value != null)
{
this.diaryDescriptions.CollectionChanged -= this.DiaryDescriptionsOnCollectionChanged;
this.diaryDescriptions =
new ObservableCollection<DiaryDescriptionDetailsViewModel>(
value
.OrderBy(s => s.Section.Section)
.ThenBy(i => i.BidItem.BidItem.Number));
this.diaryDescriptions.CollectionChanged += this.DiaryDescriptionsOnCollectionChanged;
}
else
{
this.diaryDescriptions = new ObservableCollection<DiaryDescriptionDetailsViewModel>();
}
this.OnPropertyChanged();
}
}
public DiaryDescriptionDetailsViewModel SelectedDiaryDescription
{
get
{
return this.selectedDiaryDescription;
}
set
{
// Always unsubscribe from events before replacing the object. Otherwise we end up with a memory leak.
if (this.selectedDiaryDescription != null)
{
this.selectedDiaryDescription.PropertyChanged -= this.ChildViewModelPropertyChanged;
}
this.selectedDiaryDescription = value;
if (value != null)
{
// If the description contains a biditem DiaryId, then we go fetch the section and biditem
// associated with the diary description.
if (value.BidItemId > 0)
{
this.selectedDiaryDescription.Section = this.GetSectionContainingBidItemId(value.BidItemId);
this.selectedDiaryDescription.BidItem = this.selectedDiaryDescription.Section.GetBidItem(value.BidItemId);
}
// Subscribe to property changed events so we can set ourself to dirty.
this.selectedDiaryDescription.PropertyChanged += this.ChildViewModelPropertyChanged;
this.selectedDiaryDescription.IsDirty = false;
}
this.OnPropertyChanged();
this.IsDirty = false;
}
}
DiaryDescriptionDetailViewModel
Working child view model.
public class DiaryDescriptionDetailsViewModel : BaseChangeNotify
{
private readonly DiaryDescription diaryDescription;
private SectionViewModel section;
private BidItemViewModel bidItem;
public DiaryDescriptionDetailsViewModel(DiaryDescription description, SectionViewModel section = null, BidItemViewModel bidItem = null)
{
this.diaryDescription = description;
if (description.BidItemId > 0)
{
this.section = section;
this.bidItem = bidItem;
}
this.IsDirty = false;
}
public DiaryDescription Description
{
get
{
return this.diaryDescription;
}
}
public int BidItemId
{
get
{
return this.diaryDescription.BidItemId;
}
}
public BidItemViewModel BidItem
{
get
{
return this.bidItem;
}
set
{
this.bidItem = value;
this.diaryDescription.BidItemId = value.BidItem.BidItemId;
this.OnPropertyChanged();
}
}
public SectionViewModel Section
{
get
{
return this.section;
}
set
{
this.section = value;
this.OnPropertyChanged();
}
}
}
DiaryPayItemEditorViewModel
And finally, the view model who is not having its collection rendered to the view.
public class DiaryPayItemEditorViewModel : BaseDiaryViewModel, IDataErrorInfo
{
private ObservableCollection<DiaryPayItemDetailViewModel> diaryPayItemDetails;
private DiaryPayItemDetailViewModel selectedDiaryPayItemDetail;
private List<DiaryPayItem> allPayItemsForSelectedBidItem;
private decimal sumOfAllPayItemsForBidItem;
public DiaryPayItemEditorViewModel()
{
}
public DiaryPayItemEditorViewModel(Project project, Diary diary, UserViewModel user, List<SectionViewModel> sections)
: base(project, diary, user)
{
this.Initialize(project, sections);
this.IsDirty = false;
}
public ObservableCollection<DiaryPayItemDetailViewModel> DiaryPayItemDetails
{
get
{
return this.diaryPayItemDetails;
}
set
{
this.diaryPayItemDetails = value;
this.OnPropertyChanged();
}
}
public DiaryPayItemDetailViewModel SelectedDiaryPayItemDetail
{
get
{
return this.selectedDiaryPayItemDetail;
}
set
{
if (this.selectedDiaryPayItemDetail != null)
{
this.selectedDiaryPayItemDetail.PropertyChanged -= this.ChildViewModelPropertyChanged;
}
if (value != null)
{
value.PropertyChanged += this.ChildViewModelPropertyChanged;
}
this.selectedDiaryPayItemDetail = value;
this.OnPropertyChanged();
}
}
private void Initialize(Project project, List<SectionViewModel> sections)
{
var repository = new DiaryRepository();
var projectRepository = new ProjectRepository();
this.DiaryPayItemDetails = new ObservableCollection<DiaryPayItemDetailViewModel>();
this.ProjectSections = sections;
// Repository calls to the database.
List<DiaryPayItem> payItems = repository.GetDiaryPayItemsByDiaryId(this.Diary.DiaryId);
var sectionItems = projectRepository.GetSectionHierarchy(project.ProjectId);
// Temporary, needs to be refined.
foreach (var diaryPayItem in payItems)
{
var subItem = sectionItems.SubItems.FirstOrDefault(sub => sub.SubItemId == diaryPayItem.SubItemId);
var bidItems =
sectionItems.BidItems.Where(bid => bid.BidItemId == subItem.BidItemId)
.Select(
bid =>
new BidItemViewModel(project, bid,
sectionItems.SubItems.Where(sub => sub.BidItemId == bid.BidItemId)));
var section = new SectionViewModel(
project,
sectionItems.Sections.FirstOrDefault(s => bidItems.Any(bid => bid.BidItem.SectionId == s.SectionId)),
bidItems);
this.DiaryPayItemDetails.Add(
new DiaryPayItemDetailViewModel(
diaryPayItem,
section,
bidItems.FirstOrDefault(bid => bid.BidItem.BidItemId == subItem.BidItemId),
subItem));
}
}
DiaryPayItemDetailViewModel - Child view model to the troublesome view model
public class DiaryPayItemDetailViewModel : BaseChangeNotify
{
private DiaryPayItem diaryPayItem;
private SectionViewModel selectedSection;
private BidItemViewModel selectedBidItem;
private SubItem selectedSubItem;
public DiaryPayItemDetailViewModel(
DiaryPayItem diaryPayItem,
SectionViewModel section,
BidItemViewModel bidItem,
SubItem subItem)
{
this.DiaryPayItem = diaryPayItem;
this.SelectedSection = section;
this.SelectedBidItem = bidItem;
this.SelectedSubItem = subItem;
}
public DiaryPayItem DiaryPayItem
{
get
{
return this.diaryPayItem;
}
set
{
this.diaryPayItem = value;
this.OnPropertyChanged();
}
}
public SectionViewModel SelectedSection
{
get
{
return this.selectedSection;
}
set
{
this.selectedSection = value;
this.OnPropertyChanged();
}
}
public BidItemViewModel SelectedBidItem
{
get
{
return this.selectedBidItem;
}
set
{
this.selectedBidItem = value;
this.OnPropertyChanged();
}
}
public SubItem SelectedSubItem
{
get
{
return this.selectedSubItem;
}
set
{
this.selectedSubItem = value;
this.DiaryPayItem.SubItemId = value.SubItemId;
this.OnPropertyChanged();
}
}
XAML for the DiaryDescription Tab Item.
<ListView ItemsSource="{Binding Path=DiaryDescriptions}"
SelectedItem="{Binding Path=SelectedDiaryDescription}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Section.SectionName}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
XAML for Diary Pay Items tab item.
<ListView Name="PayItemListView"
ItemsSource="{Binding Path=DiaryPayItemDetails}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=SelectedBidItem.BidItem.Description}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
BaseChangeNotify
Lastly, to show my INotifyPropertyChanged implementation, I present my base class. It wraps all calls to the event handlers in an Application.Current.Dispatcher.Invoke() action. This forces all event handler calls to be ran on the main thread so I don't have to worry about cross-thread issues in my inherited objects.
public class BaseChangeNotify : INotifyPropertyChanged
{
private bool isDirty;
private UserViewModel user;
public BaseChangeNotify()
{
}
public BaseChangeNotify(UserViewModel user)
{
this.user = user;
}
public event PropertyChangedEventHandler PropertyChanged;
public bool IsDirty
{
get
{
return this.isDirty;
}
set
{
this.isDirty = value;
this.OnPropertyChanged();
}
}
public UserViewModel User
{
get
{
return this.user;
}
}
public virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
// Perform the IsDirty check so we don't get stuck in a infinite loop.
if (propertyName != "IsDirty")
{
this.IsDirty = true; // Each time a property value is changed, we set the dirty bool.
}
if (this.PropertyChanged != null)
{
// Invoke the event handlers attached by other objects.
try
{
// When unit testing, this will always be null.
if (Application.Current != null)
{
Application.Current.Dispatcher.Invoke(() =>
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)));
}
else
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
catch (Exception)
{
throw;
}
}
}
If anyone could help me figure this out I would greatly appreciate it. I have been trying various things for the last two days and can't get it figured out. It's weird how one view model works fine, essentially performing the same kind of operation, and the other does not.
Thanks in advance.
The DiaryEditorViewModel is the view model to a DiaryEditorWindow. The DiaryPayItemEditorViewModel belongs to a user control that resides within the Window. Setting the data context in the XAML at the Window level, for the TabItem, resolved this issue. Setting the DataContext at the UserControl level caused view model to not bind properly.
I also tried setting the datacontext in the constructor, but that had the same issue. It would never bind. By setting the datacontext in the XAML of the TabItem associated with the troublesome view model, the problem was resolved. I don't understand why this is an issue. Since the view model fully implements the property changed event, I should be able to set the data context at any point, and adjust the values without a problem.
Eitherway, I have been able to solve this issue.

Programmatically WPF listbox.ItemsSource update

I am doing a program like messenger that has all the contacts in a listbox with the relative states of the contacts.
Cyclic I get a xml with the contacts were updated over time, then updates the states within a class of binding called "Contacts".
The class Contacts has a filter to display only certain contacts by their state, "online, away, busy,.. " but not offline, for example ....
Some code:
public class Contacts : ObservableCollection<ContactData>
{
private ContactData.States _state = ContactData.States.Online | ContactData.States.Busy;
public ContactData.States Filter { get { return _state; } set { _state = value; } }
public IEnumerable<ContactData> FilteredItems
{
get { return Items.Where(o => o.State == _state || (_state & o.State) == o.State).ToArray(); }
}
public Contacts()
{
XDocument doc = XDocument.Load("http://localhost/contact/xml/contactlist.php");
foreach (ContactData data in ContactData.ParseXML(doc)) Add(data);
}
}
Update part:
void StatusUpdater(object sender, EventArgs e)
{
ContactData[] contacts = ((Contacts)contactList.Resources["Contacts"]).ToArray<ContactData>();
XDocument doc = XDocument.Load("http://localhost/contact/xml/status.php");
foreach (XElement node in doc.Descendants("update"))
{
var item = contacts.Where(i => i.UserID.ToString() == node.Element("uid").Value);
ContactData[] its = item.ToArray();
if (its.Length > 0) its[0].Data["state"] = node.Element("state").Value;
}
contactList.ListBox.ItemsSource = ((Contacts)contactList.Resources["Contacts"]).FilteredItems;
}
My problem is that when ItemsSource reassigns the value of the listbox, the program lag for a few seconds, until it has finished updating contacts UI (currently 250 simulated).
How can I avoid this annoying problem?
Edit:
I tried with Thread and after with BackgroundWorker but nothing is changed...
When i call Dispatcher.Invoke lag happen.
Class ContactData
public class ContactData : INotifyPropertyChanged
{
public enum States { Offline = 1, Online = 2, Away = 4, Busy = 8 }
public event PropertyChangedEventHandler PropertyChanged;
public int UserID
{
get { return int.Parse(Data["uid"]); }
set { Data["uid"] = value.ToString(); NotifyPropertyChanged("UserID"); }
}
public States State
{
get { return (States)Enum.Parse(typeof(States), Data["state"]); }
//set { Data["state"] = value.ToString(); NotifyPropertyChanged("State"); }
//correct way to update, i forgot to notify changes of "ColorState" and "BrushState"
set
{
Data["state"] = value.ToString();
NotifyPropertyChanged("State");
NotifyPropertyChanged("ColorState");
NotifyPropertyChanged("BrushState");
}
}
public Dictionary<string, string> Data { get; set; }
public void Set(string name, string value)
{
if (Data.Keys.Contains(name)) Data[name] = value;
else Data.Add(name, value);
NotifyPropertyChanged("Data");
}
public Color ColorState { get { return UserStateToColorState(State); } }
public Brush BrushState { get { return new SolidColorBrush(ColorState); } }
public string FullName { get { return Data["name"] + ' ' + Data["surname"]; } }
public ContactData() {}
public override string ToString()
{
try { return FullName; }
catch (Exception e) { Console.WriteLine(e.Message); return base.ToString(); }
}
Color UserStateToColorState(States state)
{
switch (state)
{
case States.Online: return Colors.LightGreen;
case States.Away: return Colors.Orange;
case States.Busy: return Colors.Red;
case States.Offline: default: return Colors.Gray;
}
}
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public static ContactData[] ParseXML(XDocument xmlDocument)
{
var result = from entry in xmlDocument.Descendants("contact")
select new ContactData { Data = entry.Elements().ToDictionary(e => e.Name.ToString(), e => e.Value) };
return result.ToArray<ContactData>();
}
}
I developed a similar software: a huge contact list with data (presence and other stuff) updating quite frequently.
The solution I used is different: instead of updating the whole itemssource everytime, that is quite expensive, implement a ViewModel class for each contact. The ViewModel class should implement INotifiyPropertyChanged.
At this point when you parse the XML, you update the ContactViewModel properties and this will trigger the correct NotifyPropertyChanged events that will update the correct piece of UI.
It might be expensive if you update a lot of properties for a lot of contacts at the same time, for that you can implement some kind of caching like:
contactViewModel.BeginUpdate()
contactViewModel.Presence = Presence.Available;
..... other updates
contactViewModel.EndUpdate(); // at this point trigger PropertyCHanged events.
Another point:
keep a separate ObservableCollection bound to the ListBox and never change the itemssource property: you risk losing the current selection, scrollposition, etc.
dynamically add/remove elements from the collection bound to the listbox.
Buon divertimento e in bocca al lupo :-)
Move the downloading and parsing of the contact status information to another thread.
The line where you assigning the ItemsSource I would put in another thread, but remember about invoking or you're gonna have irritating errors.

Categories