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.
Related
I have a ListView in XAML that is bound to an ObservableCollection in the ViewModel. Upon initialization or OnAppearing() the ListView items are displayed perfectly.
However, when I try to update the ListView items from within the page (through ViewModel) the items are updated but the old items are still there.
Basically, the new items are added to the ListView but below the items that were in the ObservableCollection before. I have implemented INotifyPropertyChanged and I think I have done everything correct (although clearly not).
Please tell me what I'm doing wrong. I've tried Clear() on the Collection but to no avail (same outcome).
BaseViewModel:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
return;
backingField = value;
OnPropertyChanged(propertyName);
}
}
XAML:
<ListView IsEnabled="{Binding IsLoadingTable, Converter={Helpers:InverseBoolConverter}}"
IsVisible="{Binding IsLoadingTable, Converter={Helpers:InverseBoolConverter}}"
ItemsSource="{Binding LeagueStandings}"
SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
ItemSelected="ListView_ItemSelected"
RowHeight="60"
SeparatorVisibility="Default"
SeparatorColor="{DynamicResource accentColor}">
Page.cs:
protected override void OnAppearing()
{
base.OnAppearing();
ViewModel.LoadLeagueStandingsCommand.Execute(_divisionId);
ViewModel.LoadPickerItemsCommand.Execute(null);
}
ViewModel Initialization:
private ObservableCollection<Standing> _leagueStandings;
public ObservableCollection<Standing> LeagueStandings
{
get { return _leagueStandings ?? (_leagueStandings = new ObservableCollection<Standing>()); }
set { SetValue(ref _leagueStandings, value); }
}
ViewModel Methods:
private async Task LoadLeagueStandings(string divId)
{
if (_hasLoadedStandings)
return;
if (IsLoadingTable)
return;
_hasLoadedStandings = true;
_divisionId = divId;
try
{
IsLoadingTable = true;
await _pageService.DisplayAlert("loading Selected", _divisionId, "ok");
var v = await GetLeagueTableAsync(_htmlParser, _divisionId);
LeagueStandings = new ObservableCollection<Standing>(v);
}
catch (Exception)
{
System.Diagnostics.Debug.WriteLine("Exception caught in DivisionsViewModel.cs.(LoadLeagueStandings).");
}
finally
{
IsLoadingTable = false;
}
}
ViewModel method called when Picker item changes:
private async Task SelectItem(string item)
{
if (item == null)
return;
SelectedItem = null;
var id = await _divisionFinder.GetDivisionIdAsync(item);
var v = await GetLeagueTableAsync(_htmlParser, id);
LeagueStandings = new ObservableCollection<Standing>(v);
}
Edit* - Picture of outcome. First collection ends at number 5 and new collections appends to end of listview starting at 1 again.
ListView Image
Edit 2:
public async Task<IEnumerable<Standing>> GetLeagueTableAsync(string divisionId)
{
// todo: get division Id from picker
string address = "";
if (IsOnline)
{
if (divisionId != "")
address = $"{BaseUrls.LeagueStandings}{divisionId}";
try
{
var config = Configuration.Default.WithDefaultLoader();
var document = await BrowsingContext.New(config).OpenAsync(address);
var cSelector = "table[class='table table-striped table-hover table-bordered'] tr";
var table = document.QuerySelectorAll(cSelector).Skip(1);
int count = 0;
foreach (var c in table)
{
var cells = c.QuerySelectorAll("td").ToArray();
_leagueStandings.Add(new Standing(++count, cells[0].TextContent.Trim(), cells[1].TextContent.Trim(),
cells[2].TextContent.Trim(), cells[3].TextContent.Trim(),
cells[4].TextContent.Trim(), cells[5].TextContent.Trim(),
cells[6].TextContent.Trim(), cells[7].TextContent.Trim()));
}
}
catch(Exception e)
{
System.Diagnostics.Debug.WriteLine($"\n\n Exception caught LoadingLeagueTable - {e} \n\n");
}
}
return _leagueStandings;
Since you're not adding neither removing items, and you're replacing the reference you need to raise the event telling that the view has changed. Instead of your code, replace it by this
private ObservableCollection<Standing> _leagueStandings;
public ObservableCollection<Standing> LeagueStandings
{
get { return _leagueStandings; }
set {
_leagueStandings = value;
RaisePropertyChanged("LeagueStandings");
}
}
Also for future references, ObservableCollection already implements INotifyPropertyChanged so you don't need to SetValue(x)..
I'm getting stuck into MVVM in WPF and I have setup an ObservableCollection and an ICollectionView. The ICollectionView is set as the ItemsSource of a DataGrid, and the model is a type of Job.
I've setup getters and setter for both of the collections however when I am setting a Filter on the ICollectionView instead of the Job being filtered by the SearchString they're just replicated over and over again, leading me to believe that they way I have the collections setup is totally wrong.
Here is how the two collections are get/set:
public ObservableCollection<Job> AllJobs
{
get
{
foreach (var job in _allJobsList)
_allJobs.Add(job);
return _allJobs;
}
set
{
if (_allJobs == value) return;
OnPropertyChanged("AllJobs");
}
}
public ICollectionView AllJobsView
{
get
{
_allJobsView = CollectionViewSource.GetDefaultView(AllJobs);
return _allJobsView;
}
set
{
if (_allJobsView == value)
{
return;
}
_allJobsView = value;
OnPropertyChanged("AllJobsView");
}
}
Now I have a stringcalled SearchString that is bound to a TextBox.Text. When the text changes I do the following:
public string SearchString
{
get => _searchString;
set
{
if (_searchString == value) return;
_searchString = value;
FilterJobs();
OnPropertyChanged("SearchString");
}
}
private void FilterJobs()
{
AllJobsView.Filter = x =>
{
var viewJob = x as Job;
return viewJob != null && viewJob.Number.Contains(_searchString);
};
}
Now when the page first loads, there are the correct Jobs loaded into the DataGrid. However, as soon as the user types the Jobs are duplicated if the Job.Number does contain the SearchString. How am I able to setup the collections so that I can appropriately set a filter?
The problem is in the getter of your ObservableCollection. Every time you "get" the collection, you are adding every item to the collection all over again.
Your code:
get
{
foreach (var job in _allJobsList)
_allJobs.Add(job);
return _allJobs;
}
Instead, it should be:
get
{
return _allJobs;
}
The setter of your ObservableCollection is also missing the "setter" (private field = value) code:
set
{
if (value != _allJobs)
{
_allJobs = value;
OnPropertyChanged("AllJobs");
}
}
Your Property AllJobs would then be:
private ObservableCollection<Job> _allJobs;
public ObservableCollection<Job> AllJobs
{
get
{
return _allJobs;
}
set
{
if (value != _allJobs)
{
_allJobs = value;
OnPropertyChanged("AllJobs");
}
}
}
The initialization of your collection should be someplace else (and not in the getter of your property), like in the constructor of the ViewModel or/and in a method that a command calls after the user asks for a refresh of the collection.
For example, if your VieModel is called MyViewModel and your List<Job> is called _allJobsList, you can initialize your collection like so:
public MyViewModel()
{
//fill the _allJobsList first, getting from a database for example: _allJobsList = GetJobs();
//and then create an observable collection from that list
AllJobs = new ObservableCollection<Job>(_allJobsList);
}
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));
I am creating varied number of MvxSpinners programmatically. The number of the MvxSpinners generated cannot be predetermined. It is determined by the user input.
I have a List<Beneficiary>. Each MvxSpinner is meant to update each Beneficiary in the collection.
Since I cannot determine the number of MvxSpinner (which corresponds to the count of the Beneficiary in the collection) to be generated, I am forced to have one ICommand to handle all the HandleSelectedItem event of the MvxSpinners.
The Challenge
I am having difficulty determining the index of the List<Beneficiary> to update depending on the MvxSpinner the user clicked.
An Example
let
var BeneficiaryList=new List<Beneficiary>()
If there are 5 Beneficiary object in the collection, 5 MvxSpinner will be generated.
If the user selects a MVXSpinner which is meant to update index 2 of the collection, how do i determine the index of Beneficary to update?
What I have tried
private IList<Beneficiary> _beneficiaryList;
public IList<Beneficiary> BeneficiaryList
{
get { return _beneficiaryList; }
set { _beneficiaryList= value; RaisePropertyChanged(() => BeneficiaryList); }
}
public ICommand UpdateBeneficiary=> new MvxCommand<Beneficiary>(item =>
{
//item is of type Beneficiary
//But I do not know which index of BeneficiaryList to update
});
Your help will be deeply appreciated.
You probably need a List of ICommands too, one for each spinner. Something like this in your view model...
private IList<ICommand> _commands;
public IList<ICommand> Commands {
get {
if (_commands == null) {
_commands = BeneficiaryList.Select(x => new MvxCommand<Beneficiary>(item => {
...
}));
}
return _commands;
}
}
And set up your bindings like this (assuming you've got a list of spinners)
for (int i = 0; i < spinners.Count; i++) {
var spinner = spinners[i];
set.Bind (spinner).To(vm => vm.Commands[i]);
}
Well, it is interesting to answer my own question.
What I did was to give each Spinner a unique ID that corresponds to the index of the collection.
I created a custom Spinner called MvxSpinnerIndexer extending MvxSpinner (I really do not think it matters. You can just extend Spinner). MvxSpinnerIndexer retrieved the Id and the SelectedItem and then placed the two into a Dictionary
Here is the source for MvxSpinnerIndexer
public class MvxSpinnerIndexer : Spinner
{
public MvxSpinnerIndexer(Context context, IAttributeSet attrs)
: this(
context, attrs,
new MvxAdapter(context)
{
SimpleViewLayoutId = global::Android.Resource.Layout.SimpleDropDownItem1Line
})
{ }
public MvxSpinnerIndexer(Context context, IAttributeSet attrs, IMvxAdapter adapter)
: base(context, attrs)
{
var itemTemplateId = MvxAttributeHelpers.ReadListItemTemplateId(context, attrs);
var dropDownItemTemplateId = MvxAttributeHelpers.ReadDropDownListItemTemplateId(context, attrs);
adapter.ItemTemplateId = itemTemplateId;
adapter.DropDownItemTemplateId = dropDownItemTemplateId;
Adapter = adapter;
SetupHandleItemSelected();
}
public new IMvxAdapter Adapter
{
get { return base.Adapter as IMvxAdapter; }
set
{
var existing = Adapter;
if (existing == value)
return;
if (existing != null && value != null)
{
value.ItemsSource = existing.ItemsSource;
value.ItemTemplateId = existing.ItemTemplateId;
}
base.Adapter = value;
}
}
[MvxSetToNullAfterBinding]
public IEnumerable ItemsSource
{
get { return Adapter.ItemsSource; }
set { Adapter.ItemsSource = value; }
}
public int ItemTemplateId
{
get { return Adapter.ItemTemplateId; }
set { Adapter.ItemTemplateId = value; }
}
public int DropDownItemTemplateId
{
get { return Adapter.DropDownItemTemplateId; }
set { Adapter.DropDownItemTemplateId = value; }
}
public ICommand HandleItemSelected { get; set; }
public int ViewId { get; set; }
private void SetupHandleItemSelected()
{
ItemSelected += (sender, args) =>
{
//sender.
var control = (MvxSpinnerIndexer)sender;
var controlId = control.Id;
var position = args.Position;
HandleSelected(position, controlId);
};
}
protected virtual void HandleSelected(int position, int? controlId)
{
var item = Adapter.GetRawItem(position);
var content = new Dictionary<string, object> {{"Index", controlId}, {"SelectedItem", item}};
//var param = new ListItemWithIndexModel { Index = controlId, SelectedItem = item };
if (HandleItemSelected == null
|| item == null
|| !HandleItemSelected.CanExecute(content))
return;
HandleItemSelected.Execute(content);
}
}
In your ViewModel
public ICommand SpinnerSelected => new MvxCommand<Dictionary<string, object>>(item =>
{
var selectedItem = item["SelectedItem"] as ListItemModel;//Cast to the actual model
var index = item["Index"] as int?;//The index of the collection to update
});
I believe this will be useful to the community.
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.