Let's say I have following models:
class Worker
{
int Id;
string firstname;
string lastname;
}
class Department
{
string title;
string description;
List<Worker> workers;
}
I want to display, on UI, department's title,description and list of workers inside listbox (in listbox I want to display only firstname and lastname).
Do I need to create ONE viewmodel that will wrap this relation or I must I create a viewmodel for every model that I have?
You can create on ViewModel Which wrap both of them like:
namespace XXXX.ViewModel
{
public class MainViewModel : ViewModelBase
{
private int _id;
private string _total;
private string _description;
private ObservableCollection<Worker> _workers;
public int Id
{
get { return _id; }
set
{
if (value == _id) return;
_id = value;
RaisePropertyChanged("Id");
}
}
public string Total
{
get { return _total; }
set
{
if (value == _total) return;
_total = value;
RaisePropertyChanged("Total");
}
}
public string Description
{
get { return _description; }
set
{
if (value == _description) return;
_description = value;
RaisePropertyChanged("Description");
}
}
public ObservableCollection<Worker> Workers
{
get { return _workers; }
set
{
if (value == _workers) return;
_workers = value;
RaisePropertyChanged("Workers");
}
}
//****************** You Logic *************************
public MainViewModel()
{
Department department = new Department();
}
//****************** You Logic *************************
}
}
You wouldn't have ViewModel for every Model, in MVVM you should have a unique ViewModel for almost every view. You would then map the Model to the ViewModel.
For example:
public class DepartmentViewModel
{
public string title { get; set; }
public string description { get; set; }
public IEnumerable<Worker> workers { get; set; }
//Additional ViewModel properties here
//These may or may not be items that exist in your Model
/// <summary>
/// Mapped to the description but truncated to 10 characters and followed by an elispe (...)
/// </summary>
public string ShortDescription
{
get
{
return description.Substring(0,10) + "...";
}
}
}
I realize at first this looks a little redundant. However, there could be other less 1:1 type of views you might create from the model.
Also check out automapper.org, this is a great tool for mapping object to object.
You have 1 view model that contains both the workers and the department.
If the view only wants to show certain attributes of the workers, then the view should do that filtering. Try using an item template:
<ListBox x:Name="_workers" ItemsSource="{Binding Workers}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding firstname}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding lastname}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The view model should contain:
private string _title;
public string Title {
get {return _title;}
set {_title = value; RaisePropertyChanged("Title");}
}
private string _description;
public string Description {
get {return _description;}
set {_description= value; RaisePropertyChanged("Description");}
}
public ObservableCollection Workers {get; private set;}
public Constructor()
{
Workers = new ObservableCollection();
}
//This method is called by the model once it has fetched data.
//This can be done as a callback or in an event handler
public CalledByTheModelAfterLoadingData(Department department)
{
Title = department.Title;
Description = department.Description;
foreach (var worker in department.Workers)
{
Workers.Add(worker);
}
}
Related
I have 3 model classes as follows (Vegetable, Fruit and ItemInBasket):
public class Vegetable
{
string name;
public string Name
{
get { return name; }
set { name = value; }
}
public Vegetable(string _Name)
{
this.Name = _Name;
}
}
public class Fruit
{
string name;
public string Name
{
get { return name; }
set { name = value; }
}
public Fruit(string _Name)
{
this.Name = _Name;
}
}
public class ItemInBasket
{
string name;
public string Name
{
get { return name; }
set { name = value; }
}
object fruitorvegetable;
public object FruitOrVegetable
{
get { return fruitorvegetable; }
set { fruitorvegetable = value; }
}
int quantity;
public int Quantity
{
get { return quantity; }
set { quantity = value; }
}
public ItemInBasket(string _Name, object _FruitOrVegetable, int _Quantity)
{
this.Name = _Name;
this.FruitOrVegetable = _FruitOrVegetable;
this.quantity = _Quantity;
}
}
My ViewModel is:
public class ViewModel
{
private ObservableCollection<object> _availableItems;
public ObservableCollection<object> AvailableItems
{
get { return _availableItems; }
set { _availableItems = value; }
}
private ObservableCollection<ItemInBasket> _itemsInBasket;
public ObservableCollection<ItemInBasket> ItemsInBasket
{
get { return _itemsInBasket; }
set { _itemsInBasket = value; }
}
public ViewModel()
{
_availableItems = new ObservableCollection<object>();
_itemsInBasket = new ObservableCollection<ItemInBasket>();
this.GenerateAvailableItems();
this.GenerateItemsInBasket();
}
private void GenerateAvailableItems()
{
_availableItems.Add(new Vegetable("Broccoli")); // index 0
_availableItems.Add(new Vegetable("Kale")); // index 1
_availableItems.Add(new Vegetable("Spinach")); // index 2
_availableItems.Add(new Vegetable("Carrots")); // index 3
_availableItems.Add(new Vegetable("Garlic")); // index 4
_availableItems.Add(new Fruit("Apple")); // index 5
_availableItems.Add(new Fruit("Orange")); // index 6
_availableItems.Add(new Fruit("Pear")); // index 7
_availableItems.Add(new Fruit("Cherry")); // index 8
_availableItems.Add(new Fruit("Grape")); // index 9
}
private void GenerateItemsInBasket()
{
_itemsInBasket.Add(new ItemInBasket("Apples",_availableItems[5],3));
_itemsInBasket.Add(new ItemInBasket("Kale", _availableItems[1], 10));
_itemsInBasket.Add(new ItemInBasket("Grape", _availableItems[9], 2));
_itemsInBasket.Add(new ItemInBasket("Carrots", _availableItems[3], 1));
}
}
I am trying to be able to modify the FruitOrVegetable inside of each ItemInBasket displayed in the datagrid but I have an issue with the data binding. I am using Syncfusion datagrid but I think it shouldnt affect anything.
<syncfusion:SfDataGrid AutoGenerateColumns="False" SelectionMode="Single"
AllowEditing="True" AllowDeleting="True" ItemsSource="{Binding ItemsInBasket,Source={StaticResource viewModel}}">
<syncfusion:SfDataGrid.Columns>
<syncfusion:GridTemplateColumn MappingName="FruitOrVegetable" HeaderText="Order">
<syncfusion:GridTemplateColumn.EditTemplate>
<DataTemplate>
<ComboBox IsEditable="True" DisplayMemberPath="Name" SelectedValuePath="Name"
Text="{Binding FruitOrVegetable.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding AvailableItems, Source={StaticResource viewModel}}"/>
</DataTemplate>
</syncfusion:GridTemplateColumn.EditTemplate>
<syncfusion:GridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</syncfusion:GridTemplateColumn.CellTemplate>
</syncfusion:GridTemplateColumn>
<syncfusion:GridNumericColumn HeaderText="Quantity" MappingName ="Quantity"/>
</syncfusion:SfDataGrid.Columns>
</syncfusion:SfDataGrid>
Your requirement to display the modified value of the FruitOrVegetable in SfDataGrid can be achieved by binding the SelectedValue property of ComboBox. Please refer to the below code snippet,
<ComboBox IsEditable="True" DisplayMemberPath="Name" SelectedValuePath="Name"
SelectedValue="{Binding Name}"
Text="{Binding FruitOrVegetable.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding AvailableItems, Source={StaticResource viewModel}}"/>
This is where what we call an "interface" comes in handy. This gives you the surety that the properties are indeed present on this object. Consider it a contract between the class and interface to always include some properties.
You can learn more about it here
You can define an interface like this
public interface INamedItem
{
string Name {get;set;}
}
Now instead of the object type use this interface(first implement them in your vegetable and fruit class) so that you can easily bind to it(and many other benefits)
public class Vegetable : INamedItem
{
string name;
public string Name
{
get { return name; }
set { name = value; }
}
public Vegetable(string _Name)
{
this.Name = _Name;
}
}
public class Fruit: INamedItem
{
string name;
public string Name
{
get { return name; }
set { name = value; }
}
public Fruit(string _Name)
{
this.Name = _Name;
}
}
public class ItemInBasket
{
string name;
public string Name
{
get { return name; }
set { name = value; }
}
INamedItem fruitorvegetable;
public object FruitOrVegetable
{
get { return fruitorvegetable; }
set { fruitorvegetable = value; }
}
int quantity;
public int Quantity
{
get { return quantity; }
set { quantity = value; }
}
public ItemInBasket(string _Name, INamedItem _FruitOrVegetable, int _Quantity)
{
this.Name = _Name;
this.FruitOrVegetable = _FruitOrVegetable;
this.quantity = _Quantity;
}
}
For your available items it becomes:
private ObservableCollection<INamedItem> _availableItems;
public ObservableCollection<INamedItem> AvailableItems
{
get { return _availableItems; }
set { _availableItems = value; }
}
Now add normally as you have in your code so no more changes to that and check if it works :)
I have the following view model:
public sealed class FileViewModel : AbstractPropNotifier
{
private string _path;
private CategoryViewModel _category;
public string Path
{
get
{
return _path;
}
set
{
_path = value;
OnPropertyChanged(nameof(Path));
OnPropertyChanged(nameof(Title));
}
}
public string Title => System.IO.Path.GetFileNameWithoutExtension(Path);
public CategoryViewModel Category
{
get
{
return _category;
}
set
{
_category = value;
OnPropertyChanged(nameof(Category));
}
}
}
and Category view model:
public sealed class CategoryViewModel : IEquatable<CategoryViewModel>
{
public string Title { get; set; }
public EMyEnum Value { get; set; }
public bool Equals(CategoryViewModel other)
{
return Title.Equals(other.Title) && Value == other.Value;
}
public static CategoryViewModel From(EMyEnum eCat)
{
return new CategoryViewModel
{
Title = eCat.DescriptionAttr(),
Value = eCat
};
}
}
I set data context to my view model like:
public sealed class MainViewModel
{
public MainViewModel()
{
Files = new ObservableCollection<FileViewModel>();
Categories = GetCategories();
}
public ObservableCollection<FileViewModel> Files { get; set; }
public CategoryViewModel[] Categories { get; set; }
private CategoryViewModel[] GetCategories()
{
var enums = Enum.GetValues(typeof(EMyEnum));
var list = new List<CategoryViewModel>();
foreach (var en in enums)
{
EMyEnum cat = (EMyEnum)en;
list.Add(CategoryViewModel.From(cat));
}
return list.ToArray();
}
}
and
_model = new MainViewModel();
DataContext = _model;
and XAML:
<Window.Resources>
<CollectionViewSource x:Key="Categories" Source="{Binding Categories}"/>
</Window.Resources>
and in DataGrid element
<DataGridComboBoxColumn SelectedItemBinding="{Binding Category}" ItemsSource="{Binding Source={StaticResource Categories}}" Header="Category" Width="2*" DisplayMemberPath="Title"/>
The dropdown is populated correctly but cannot select automatically from dropdown a specific Category, means the Category column from Datagrid is empty.
I expected to select automatically from dropdown with correspondent Category...
Where is my mistake ? I tried with SelectedItemBinding and SelectedValueBinding but same issue. Nothing selected from dropdown.
To be clear:
For a file, I set a category but nothing is selected:
But dropdown has items:
There are probably different instances of CategoryViewModels in your MainViewModel compared to the ones in the FileViewModels.
You should either override Equals and GetHashCode in your CategoryViewModel class or make sure that you set the Category property of each FileViewModel to a CategoryViewModel that's actually present in the CategoryViewModel[] array of the MainViewModel.
EDIT: This is a Windows Store (8.1) application
I have a person class as shown below which I am using as a model
public class Person : BaseModel
{
private string _FirstName;
public string FirstName
{
get { return _FirstName; }
set
{
if (_FirstName == value)
return;
_FirstName = value;
RaisePropertyChanged("FirstName");
}
}
private string _MiddleName;
public string MiddleName
{
get { return _MiddleName; }
set
{
if (_MiddleName == value)
return;
_MiddleName = value;
RaisePropertyChanged("MiddleName");
}
}
private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
if (_LastName == value)
return;
_LastName = value;
RaisePropertyChanged("LastName");
}
}
}
where BaseModel is defined as below
using GalaSoft.MvvmLight;
public class BaseModel: ObservableObject
{
}
I am using the Model in a ViewModel class as shown below.
using GalaSoft.MvvmLight;
public class PersonViewModel : ViewModelBase
{
/// <summary>
/// List of searched People
/// </summary>
private ObservableCollection<Person> _People;
public ObservableCollection<Person> People
{
get { return _People; }
set
{
if (_People== value)
return;
_People= value;
RaisePropertyChanged("People");
}
}
}
I am binding the People collection to a GridView as shown below.
<GridView
x:Name="PeopleSearchResultsGridView"
ItemsSource="{Binding People}">
</GridView>
When the search completes I get back a list of people which I add to the list as follows.
var list = await p.SearchPeople();
People = new ObservableCollection<Person>(list);
I see that the setter for the People collection is firing and the RaisePropertyChanged("People") event is also firing however that is not updating the GridView. Can anyone tell me what is wrong here ?
Update
Managed to fix the selectedIndex problem. I'd forgotten to set SelectedItem as well and naturally that caused a few issues.
So at 9AM this morning we got our 24 hour assignment and I have hit a brick wall.
We're supposed to create a program that allows a supervisor to Add and delete Employees and add Working Sessions, total hours and total earnings. But I am having some problems succesfully implementing this following the MVVM-Pattern. For some reason my Bindings simply aren't working and the only Solution I can see is someone looking over my project and helping me troubleshoot it.
Here is my code - I'm very sorry about having to post the entire thing but given that I have no clue where the problem is I did not see any other options. :
EmployeeModel
[Serializable]
public class WorkSessions : ObservableCollection<WorkSessionModel>
{
public WorkSessions()
{
}
}
[Serializable]
public class WorkSessionModel : INotifyPropertyChanged
{
private DateTime _dateTime;
private string _id;
private double _hours;
public WorkSessionModel()
{
}
public DateTime DateTime
{
get { return _dateTime; }
set
{
_dateTime = value;
NotifyPropertyChanged("DateTime");
}
}
public string ID
{
get { return _id; }
set
{
_id = value;
NotifyPropertyChanged("ID");
}
}
public double Hours
{
get { return _hours; }
set
{
_hours = value;
NotifyPropertyChanged("Hours");
NotifyPropertyChanged("TotalHours");
}
}
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
WorkSessionModel
[Serializable]
public class WorkSessions : ObservableCollection<WorkSessionModel>
{
public WorkSessions()
{
}
}
[Serializable]
public class WorkSessionModel : INotifyPropertyChanged
{
private DateTime _dateTime;
private string _id;
private double _hours;
public WorkSessionModel()
{
}
public DateTime DateTime
{
get { return _dateTime; }
set
{
_dateTime = value;
NotifyPropertyChanged("DateTime");
}
}
public string ID
{
get { return _id; }
set
{
_id = value;
NotifyPropertyChanged("ID");
}
}
public double Hours
{
get { return _hours; }
set
{
_hours = value;
NotifyPropertyChanged("Hours");
NotifyPropertyChanged("TotalHours");
}
}
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
EmployeeViewModel
public class EmployeeViewModel : ViewModelBase
{
private Employees _employeesModel = new Employees();
public Employees EmployeesView = new Employees();
public ObservableCollection<WorkSessionModel> WorkSessions { get; set; }
private string _id = "0";
private string _name = "noname";
private double _wage = 0;
private int _totalhours = 0;
public string ID
{
get { return _id; }
set { _id = value; RaisePropertyChanged("ID"); }
}
public string Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("Name");
}
}
public double Wage
{
get { return _wage; }
set
{
_wage = value;
RaisePropertyChanged("Wage");
}
}
public int TotalHours
{
get { return _totalhours; }
set
{
_totalhours = value;
RaisePropertyChanged("TotalHours");
}
}
private EmployeeModel _selectedEmployee = new EmployeeModel();
public EmployeeModel SelectedEmployee
{
get { return _selectedEmployee; }
set
{
_selectedEmployee = value;
RaisePropertyChanged("SelectedEmployee");
}
}
private int _selectedEmployeeIndex;
public int SelectedEmployeeIndex
{
get { return _selectedEmployeeIndex; }
set
{
_selectedEmployeeIndex = value;
RaisePropertyChanged("SelectedEmployeeIndex");
}
}
#region RelayCommands
// Employee Relay Commands
public RelayCommand EmployeeAddNewCommand { set; get; }
public RelayCommand EmployeeDeleteCommand { set; get; }
public RelayCommand EmployeeNextCommand { set; get; }
public RelayCommand EmployeePrevCommand { set; get; }
public RelayCommand EmployeeTotalHoursCommand { get; set; }
#endregion
public EmployeeViewModel()
{
InitCommands();
}
private void InitCommands()
{
EmployeeAddNewCommand = new RelayCommand(EmployeeAddNewExecute, EmployeeAddNewCanExecute);
EmployeeDeleteCommand = new RelayCommand(EmployeeDeleteNewExecute, EmployeeDeleteCanExecute);
EmployeeNextCommand = new RelayCommand(EmployeeNextExecute, EmployeeNextCanExecute);
EmployeePrevCommand = new RelayCommand(EmployeePrevExecute, EmployeePrevCanExecute);
//EmployeeTotalHoursCommand = new RelayCommand(EmployeeTotalHoursExecute, EmployeeTotalHoursCanExecute);
}
//private void EmployeeTotalHoursExecute()
//{
// _selectedEmployee.TotalHours();
//}
//private bool EmployeeTotalHoursCanExecute()
//{
// return true;
//}
private void EmployeeAddNewExecute()
{
EmployeeModel newEmployee = new EmployeeModel();
EmployeesView.Add(newEmployee);
_employeesModel.Add(newEmployee);
SelectedEmployee = newEmployee;
}
private bool EmployeeAddNewCanExecute()
{
return true;
}
private void EmployeeDeleteNewExecute()
{
if (MessageBox.Show("You are about delete all submissions for Employee," + SelectedEmployee.Name + "(" + SelectedEmployee.ID +")\r\nAre you sure?", "This is a Warning!", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
_employeesModel.Remove(SelectedEmployee);
EmployeesView.Remove(SelectedEmployee);
}
}
private bool EmployeeDeleteCanExecute()
{
if (SelectedEmployee != null)
return true;
else return false;
}
private void EmployeeNextExecute()
{
SelectedEmployeeIndex++;
}
private bool EmployeeNextCanExecute()
{
if (SelectedEmployeeIndex < EmployeesView.Count - 1)
return true;
return false;
}
private void EmployeePrevExecute()
{
SelectedEmployeeIndex--;
}
private bool EmployeePrevCanExecute()
{
if (SelectedEmployeeIndex > 0)
return true;
return false;
}
}
View
public partial class MainWindow : Window
{
public EmployeeViewModel EmployeeViewModel = new EmployeeViewModel();
public MainWindow()
{
InitializeComponent();
menu_employee.DataContext = EmployeeViewModel;
sp_employees.DataContext = EmployeeViewModel;
datagrid_employees.ItemsSource = EmployeeViewModel.EmployeesView;
grid_selectedEmployee.DataContext = EmployeeViewModel.SelectedEmployee;
}
}
I can see a few problems with your code:
When the SelectedIndex is updated, SelectedItem remains the same and vice versa.
It looks like your data binding is out of order:
The DataContext property cascades down to every child of a certain dependency object.
The code in the MainWindow constructor should probably be replaced by:
this.DataContext = EmployeeViewModel;
Then in XAML set the rest of the properties using Data Binding. The problem in your situation is that the DataContext of the selectedemployee is only set once. This means if you select another employee, then it will not update.
An example for your SelectedEmployee grid:
<Grid Name="grid_selectedEmployee" DataContext="{Binding SelectedEmployee,
UpdateSourceTrigger=PropertyChanged}">...</Grid>
One of the biggest things I see is you are setting properties, not binding them.
For example,
datagrid_employees.ItemsSource = EmployeeViewModel.EmployeesView;
You are telling your DataGrid that it's ItemsSource should be that specific object. You need to bind it to that value so you are telling it to point to that property instead. This will make your UI correctly reflect what's in your ViewModel
The other huge red flag I see is your ViewModel referencing something called and EmployeeView which leads me to believe your View and ViewModel too closely tied together.
Your ViewModel should contain all your business logic and code, while the View is usually XAML and simply reflects the ViewModel in a user-friendly way.
The View and the ViewModel should never directly reference each other (I have had my View reference my ViewModel in some rare occasions, but never the other way around)
For example, an EmployeesViewModel might contain
ObservableCollection<Employee> Employees
Employee SelectedEmployee
ICommand AddEmployeeCommand
ICommand DeleteEmployeeCommand
while your View (XAML) might look like this:
<StackPanel>
<StackPanel Orientation="Horizontal">
<Button Content="Add" Command="{Binding AddEmployeeCommand}" />
<Button Content="Delete" Command="{Binding DeleteEmployeeCommand}" />
</StackPanel>
<DataGrid ItemsSource="{Binding Employees}"
SelectedItem="{Binding SelectedEmployee}">
... etc
</DataGrid>
<UniformGrid DataContext="{Binding SelectedEmployee}" Columns="2" Rows="4">
<TextBlock Text="ID" />
<TextBox Text="{Binding Id}" />
... etc
</UniformGrid >
</StackPanel>
And the only thing you should be setting is the DataContext of the entire Window. Usually I overwrite App.OnStartup() to start up my application:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var view = new MainWindow();
var vm = new EmployeesViewModel;
view.DataContext = vm;
view.Show();
}
}
Although I suppose in your case this would also work:
public MainWindow()
{
InitializeComponent();
this.DataContext = new EmployeesViewModel();
}
I have two classes:
public class Person
{
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
public ObservableCollection<AccountDetail> Details
{
get { return details; }
set { details = value; }
}
public ObservableCollection<AccountDetail> Phones
{
get
{
ObservableCollection<AccountDetail> phones;
phones = new ObservableCollection<AccountDetail>();
foreach (AccountDetail detail in Details)
{
if (detail.Type == DetailType.Phone)
{
phones.Add(detail);
}
}
return phones;
}
set
{
ObservableCollection<AccountDetail> phones;
phones = value;
foreach (AccountDetail detail in Details)
{
if (detail.Type == DetailType.Phone)
{
Details.Remove(detail);
}
}
foreach (AccountDetail detail in phones)
{
if (!string.IsNullOrEmpty(detail.Value))
{
Details.Add(detail);
}
}
}
}
private string firstName;
private string lastName;
private ObservableCollection<AccountDetail> details;
}
and
public class AccountDetail
{
public DetailType Type
{
get { return type; }
set { type = value; }
}
public string Value
{
get { return this.value; }
set { this.value = value; }
}
private DetailType type;
private string value;
}
In my XAML file I have a ListBox named PhonesListBox which is data bound to the phones list (a property of the Person object):
<Window.Resources>
<!-- Window controller -->
<contollers:PersonWindowController
x:Key="WindowController" />
</Window.Resources>
...
<ListBox
Name="PhonesListBox"
Margin="0,25,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
ItemsSource="{Binding Path=SelectedPerson.Phones,
Source={StaticResource ResourceKey=WindowController}}"
HorizontalContentAlignment="Stretch" />
...
In its code behind class, there's a handler for a button which adds a new item to that PhonesListBox:
private void AddPhoneButton_Click(object sender, RoutedEventArgs e)
{
ObservableCollection<AccountDetail> phones;
phones = (ObservableCollection<AccountDetail>)PhonesListBox.ItemsSource;
phones.Add(new AccountDetail(DetailType.Phone));
}
The problem is, the newly added list box item is not added in the person's details observable collection, i.e. the Phones property is not updated (set is never called). Why? Where am I making a mistake?
Thanks for all the help.
UPDATE: I changed the AddPhoneButton_Click method to:
private void AddPhoneButton_Click(object sender, RoutedEventArgs e)
{
PersonWindowController windowController;
ObservableCollection<AccountDetail> details;
windowController = (PersonWindowController)this.FindResource("WindowController");
details = windowController.SelectedPerson.Details;
details.Add(new AccountDetail(DetailType.Phone));
}
This updates the appropriate collection, which is details not Phones (as phones is just a view or a getter of a subset of detail items). Also, I realized I don't even need the Phones setter. The problem I am facing now is that my UI is not updated with the changes made to the details collection (and subsequently phones). I don't know how or where to call for the property changed as neither details nor phones are changing; their collection members are. Help. Please.
Why do you create a new ObservableCollection<AccountDetail> each time the Phones property is retrieved? Typically the Person class would have a member field of type ObservableCollection<AccountDetail> that would just be returned in the getter for the Phones property, instead of creating a new one each time. You could populate this collection when an instance of Person is constructed, for example.
I don't know if this would fix your problem or not, but it seems like it should help.
it sounds like you have an ObservableCollection<AccountDetail> with more than just phones in it, so it looks like you actually need a CollectionViewSource with a Filter added:
public class Person
{
public ObservableCollection<AccountDetail> Details { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
<Window.Resources>
<CollectionViewSource x:Key="phonesSource"
Source="{StaticResource ResourceKey=WindowController}"
Path="SelectedPerson.Details" />
</Window.Resources>
<Grid>
<ListBox
Name="PhonesListBox"
Margin="0,25,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
ItemsSource="{Binding Source={StaticResource phonesSource}}"
HorizontalContentAlignment="Stretch" />
</Grid>
public MainWindow()
{
InitializeComponent();
CollectionViewSource source =
(CollectionViewSource)FindResource("phonesSource");
source.Filter += (o, e) =>
{
if (((AccountDetail) e.Item).Type == DetailType.Phone)
e.Accepted = true;
};
}
Changing you Person class to something like following should work :
public class Person
{
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
public ObservableCollection<AccountDetail> Details
{
get { return details; }
set { details = value; }
}
public ObservableCollection<AccountDetail> Phones
{
get
{
if (phones == null)
{
phones = new ObservableCollection<AccountDetail>();
}
phones.Clear();
foreach (AccountDetail detail in Details)
{
if (detail.Type == DetailType.Phone)
{
phones.Add(detail);
}
}
return phones;
}
set
{
phones.Clear();
foreach (var item in value)
{
phones.Add(item);
}
foreach (AccountDetail detail in Details)
{
if (detail.Type == DetailType.Phone)
{
Details.Remove(detail);
}
}
foreach (AccountDetail detail in phones)
{
if (!string.IsNullOrEmpty(detail.Value))
{
Details.Add(detail);
}
}
}
}
private string firstName;
private string lastName;
private ObservableCollection<AccountDetail> details;
public ObservableCollection<AccountDetail> phones;
}
The code is not tested and it may require a few changes from you to actually work.
Try something like:
public class Person : INotifyPropertyChanged
{
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
public ObservableCollection<AccountDetail> Details
{
get { return details; }
set { details = value; }
}
public void AddDetail(AccountDetail detail) {
details.Add(detail);
OnPropertyChanged("Phones");
}
public IEnumerable<AccountDetail> Phones
{
get
{
return details.Where(d => d.Type == DetailType.Phone);
}
}
private string firstName;
private string lastName;
private ObservableCollection<AccountDetail> details;
/// <summary>
/// Called when a property changes.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#region INotifyPropertyChanged Members
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
You could call the AddDetail method from you button event handler:
private void AddPhoneButton_Click(object sender, RoutedEventArgs e)
{
PersonWindowController windowController;
ObservableCollection<AccountDetail> details;
windowController = (PersonWindowController)this.FindResource("WindowController");
windowController.SelectedPerson.AddDetail(new AccountDetail(DetailType.Phone));
}
Raising the OnPropertyChanged event for the Phones property will simply cause the WPF binding framework to requery the propery, and the Linq query to be re-evaluated, after you added a Phone detail to the list of Details.