Reputation: 581
I'd like to use a nested ObservableCollection in MVVM in order to add as many groups and users as possible. However, I don't know how to create/add a new user. I also don't know how to bind the new users to the XAML. (NOTE: this time, I just need two persons.)
I'm new to C#, WPF and MVVM. I'm learning MVVM referring to this site: https://riptutorial.com/wpf/example/992/basic-mvvm-example-using-wpf-and-csharp I've been trying this since last week, but no luck.
I tried:
outerObservableCollection.Add(
new ObservableCollection<User>
{
{
FirstName = "Jane",
LastName = "June",
BirthDate = DateTime.Now.AddYears(-20)
}
}
);
... which ends up with the following error:
The name 'BirthDate' does not exist in the current context
(I guess the cause of this issue is that I didn't create a 'user' object, so 'user.BirthDate' is not accessible.)
Let me show the entire codes.
MainWindow.xaml:
<Window x:Class="MVVM_RIP_Tutorial.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:MVVM_RIP_Tutorial"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="32.8"/>
<RowDefinition Height="28.8"/>
<RowDefinition Height="38*"/>
<RowDefinition Height="37*"/>
<RowDefinition Height="38*"/>
<RowDefinition Height="155*"/>
</Grid.RowDefinitions>
<!--1st Person-->
<TextBlock Grid.Column="1" Grid.Row="0" Margin="320.6,4,398.6,3.2" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold" Width="0"/>
<Label Grid.Column="0" Grid.Row="1" Margin="0,4.8,4.4,2.8" Content="First Name:" HorizontalAlignment="Right" Width="70"/>
<!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. -->
<TextBox Grid.Column="1" Grid.Row="1" Margin="3.6,4.8,0,1.8" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="2" Margin="0,5.2,4.4,1.6" Content="Last Name:" HorizontalAlignment="Right" Width="69"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="3.6,5.2,0,1.6" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="3" Margin="0,6.4,4.4,4.2" Content="Age:" HorizontalAlignment="Right" Grid.RowSpan="2" Width="33"/>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="3.6,6.4,0,10.2" Text="{Binding Age}" HorizontalAlignment="Left" Grid.RowSpan="2" Width="0"/>
<!--2nd Person-->
<!--<TextBlock Grid.Column="1" Grid.Row="4" Margin="320.6,4,398.6,3.2" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold" Width="0"/>
<Label Grid.Column="0" Grid.Row="5" Margin="0,4.8,4.4,2.8" Content="First Name:" HorizontalAlignment="Right" Width="70"/>
--><!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. --><!--
<TextBox Grid.Column="1" Grid.Row="5" Margin="3.6,4.8,0,1.8" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="6" Margin="0,5.2,4.4,1.6" Content="Last Name:" HorizontalAlignment="Right" Width="69"/>
<TextBox Grid.Column="1" Grid.Row="6" Margin="3.6,5.2,0,1.6" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="7" Margin="0,6.4,4.4,4.2" Content="Age:" HorizontalAlignment="Right" Grid.RowSpan="2" Width="33"/>
<TextBlock Grid.Column="1" Grid.Row="7" Margin="3.6,6.4,0,10.2" Text="{Binding Age}" HorizontalAlignment="Left" Grid.RowSpan="2" Width="0"/>-->
</Grid>
</Window>
MainWindow.xaml.cs:
using System.Windows;
namespace MVVM_RIP_Tutorial
{
public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MyViewModel();
// The DataContext serves as the starting point of Binding Paths
DataContext = _viewModel;
}
}
}
User.cs:
using System;
namespace MVVM_RIP_Tutorial
{
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
}
MyViewModel.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MVVM_RIP_Tutorial
{
// INotifyPropertyChanged notifies the View of property changes, so that Bindings are updated.
sealed class MyViewModel : INotifyPropertyChanged
{
private User user;
ObservableCollection<ObservableCollection<User>> outerObservableCollection
= new ObservableCollection<ObservableCollection<User>>();
//ObservableCollection<User> user = new ObservableCollection<User>();
public string FirstName
{
get { return user.FirstName; }
set
{
if (user.FirstName != value)
{
user.FirstName = value;
OnPropertyChange("FirstName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
public string LastName
{
get { return user.LastName; }
set
{
if (user.LastName != value)
{
user.LastName = value;
OnPropertyChange("LastName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
// This property is an example of how model properties can be presented differently to the View.
// In this case, we transform the birth date to the user's age, which is read only.
public int Age
{
get
{
DateTime today = DateTime.Today;
int age = today.Year - user.BirthDate.Year;
if (user.BirthDate > today.AddYears(-age)) age--;
return age;
}
}
// This property is just for display purposes and is a composition of existing data.
public string FullName
{
get { return FirstName + " " + LastName; }
}
public MyViewModel()
{
user = new User
{
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now.AddYears(-30)
};
//outerObservableCollection.Add(user);
//outerObservableCollection.Add(
// new ObservableCollection<User>
// {
// {
// FirstName = "Jane",
// LastName = "June",
// BirthDate = DateTime.Now.AddYears(-20)
// }
// }
//);
);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
... Please help me. Thank you in advance.
Upvotes: 1
Views: 1788
Reputation: 226
First of all, welcome to C#, WPF, and MVVM!
From your description it sounds like you would like to display a tree of users within groups... with that in mind, you could implement something like this to accomplish that goal:
public class GroupModel
{
public GroupModel(uint id, string name)
{
Id = id;
Name = name;
}
public uint Id { get; }
public string Name { get; }
}
public class UserModel
{
public UserModel(uint id, string firstName, string surname, DateTime dateOfBirth)
{
Id = id;
FirstName = firstName;
Surname = surname;
DateOfBirth = dateOfBirth;
}
public uint Id { get; }
public string FirstName { get; }
public string Surname { get; }
public DateTime DateOfBirth { get; }
}
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public abstract class ViewModelBase<TModel> : ViewModelBase
where TModel : class
{
private TModel _model;
public ViewModelBase(TModel model)
=> _model = model;
/*
* There is a design choice here to allow the model
* to be swapped at runtime instead or to treat the
* view model as immutable in which case the setter
* for the Model property should be removed.
*/
public TModel Model
{
get => _model;
set
{
if (ReferenceEquals(_model, value))
{
return;
}
_model = value;
OnPropertyChanged();
}
}
}
Concrete classes
public class GroupViewModel : ViewModelBase<GroupModel>
{
public GroupViewModel(GroupModel model)
: base(model)
{
}
public ObservableCollection<UserViewModel> Users { get; }
= new ObservableCollection<UserViewModel>();
public void AddUser(UserModel user)
{
var viewModel = new UserViewModel(user);
Users.Add(viewModel);
}
}
public class UserViewModel : ViewModelBase<UserModel>
{
public UserViewModel(UserModel model)
: base(model)
{
}
// convenience property; could be done completely in XAML as well
public string FullName => $"{Model.FirstName} {Model.Surname}";
}
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
// create sample user groups
for (var groupIndex = 1u; groupIndex <= 5u; ++groupIndex)
{
var groupName = $"Group {groupIndex}";
var groupModel = new GroupModel(groupIndex, groupName);
var groupViewModel = new GroupViewModel(groupModel);
UserGroups.Add(groupViewModel);
for (var userIndex = 1u; userIndex <= 5u; ++userIndex)
{
var userModel = new UserModel(
id: userIndex,
firstName: "John",
surname: $"Smith",
dateOfBirth: DateTime.Today);
groupViewModel.AddUser(userModel);
}
}
}
public ObservableCollection<GroupViewModel> UserGroups { get; }
= new ObservableCollection<GroupViewModel>();
}
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:UserGroups.ViewModels"
x:Class="UserGroups.Views.MainWindow"
Title="User Groups"
Width="1024"
Height="768">
<Window.DataContext>
<viewModels:MainViewModel />
</Window.DataContext>
<Grid>
<TreeView ItemsSource="{Binding Path=UserGroups}">
<TreeView.Resources>
<!-- Template for Groups -->
<HierarchicalDataTemplate DataType="{x:Type viewModels:GroupViewModel}"
ItemsSource="{Binding Path=Users}">
<TextBlock Text="{Binding Path=Model.Name}" />
</HierarchicalDataTemplate>
<!-- Template for Users -->
<DataTemplate DataType="{x:Type viewModels:UserViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Model.Id, StringFormat='[{0}]'}"
Margin="3,0" />
<TextBlock Text="{Binding Path=FullName}"
Margin="3,0" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
Here's what you end up with:
There are lots of frameworks that take care of a lot of the tedious work of working with the MVVM pattern (e.g. removing most/all of the boilerplate code for INotifyPropertyChanged). Here are just a few to look at:
Some additional resources that might also be useful:
Upvotes: 1
Reputation: 421
It's not entirely clear to me what the result is supposed to look like, but here are some initial suggestions.
I wouldn't try nesting an Observable collection inside another one. Instead, define something like a separate Group model class that has a list of User objects as its field.
I take it the user is supposed to enter values for your bound properties in the xaml in order to create a new user? You need to add a button or something that the user can press after filling those values out. The button click should be bound to a RelayCommand (add MVVMLight to the project if necessary) in the view model. The method invoked by said relaycommand would instantiate a new User object using the fields bound in the xaml and add to your ObservableCollection.
<Button Command="{Binding Path=CreateUserCommand}">
<TextBlock Text="Create User"/>
</Button>
Then in the view model...
public RelayCommand CreateUserCommand { get; private set; }
CreateUserCommand = new RelayCommand(() =>
{
User user = new User
{
FirstName = FirstName,
LastName = LastName,
//...etc.
}
collectionOfUsers.Add(user);
});
So far I don't see any xaml code that would handle displaying new users. Seems to me you'd want to bind your collection of users to a grid or combo box. After the user enters new user properties in the textboxes and clicks the appropriate button, the grid or combo box would update. You could have separate controls for separate groups. Again, that part is not entirely clear to me.
Hope that helps.
Upvotes: 1