I have a person list filled from the database, two fields (id, name) from the person object are databinded with a radiobutton, now when I click on the radiobutton I’m opening a new UserControl and trying to show all the fields of the selected object in the UserControl in detail.
how can I implement this within mvvm?
PersonModel:
namespace Persons.Models
{
class PersonModel
{
public int PersonID { get; set; }
public string Name { get; set; }
public string Surname {get; set; }
public string PersonAge { get; set; }
public string PersonAddress { get; set; }
public string PersonDepartmentID{ get; set; }
public string PersonGroupID{ get; set; }
public string PersonContact{ get; set; }
public string PersonLevelEductationID{ get; set; }
public string PersonDivisionID{get; set; }
public ObservableCollection<PersonModel> Get()
{
ObservableCollection<PersonModel> Person_list = new ObservableCollection<PersonModel>();
var Connect = ConfigurationManager.ConnectionStrings["MysqlConnection"].ConnectionString;
using (var conn = new MySqlConnection(Connect))
using (var command = new MySqlCommand())
{
conn.Open();
command.Connection = conn;
command.CommandText = @"SELECT person.id, person.name, person.surname, person.address, person.age, person.contact department.name AS 'department', group.name AS 'group', eductation.lavel division.name AS 'division' FROM person
LEFT JOIN department ON department.id = person.departmentID LEFT JOIN group ON group.id = person.groupID LEFT JOIN eductation ON eductation.id = person.eductationLavelID LEFT JOIN division ON division.id = person.divisionID";
MySqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
Person_list.Add(new PersonModel()
{
PersonID = (int)reader["id"],
Name = (string)reader["name"],
Surname = (string)reader["surname"],
PersonAge = (int)reader["age"],
PersonAddress = (string)reader["address"],
PersonDepartmentID = (string)reader["department"],
PersonGroupID= (string)reader["group"],
PersonContact = (string)reader["contact"],
PersonLevelEductationID = (string)reader["lavel"],
PersonDivisionID = (string)reader["division"]
}
) ;
}
reader.Close();
conn.Close();
}
return Person_list;
}
}
}
ViewModels
namespace Persons.ViewModels
{
class PersonViewModel : ViewModelBase
{
private ObservableCollection<PersonModel> _personList;
private readonly PersonModel _person_Model;
public ObservableCollection<PersonModel> PersonItems
{
get => _personList;
set
{
if (_personList!= value)
{
_personList = value;
OnpropertyChanged(nameof(PersonItems));
}
}
}
public PersonViewModel()
{
_person_Model= new PersonModel();
_personList= _person_Model.Get();
}
}
}
Main UserControl :
<ScrollViewer
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding PersonItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<RadioButton
Content="{Binding Name}"
CommandParameter="{Binding Name}"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ShowPageCommand}"
Style="{StaticResource ItemsButtonStyle}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
And New UserControl
<ScrollViewer
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding PersonItems}" BorderBrush="White">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Width="600"
MaxHeight="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock
HorizontalAlignment="Left"
Grid.Row="1"
Grid.Column="0"
Text="Name:" />
<TextBlock
HorizontalAlignment="Right"
Grid.Row="1"
Grid.Column="0"
Text="{Binding Name}" />
<TextBlock
HorizontalAlignment="Left"
Grid.Row="2"
Text="Surname :" />
<TextBlock
HorizontalAlignment="Right"
Grid.Row="2"
Text="{Binding Surname}"/>
<TextBlock
HorizontalAlignment="Left"
Grid.Row="3"
Text="PersonAge :" />
<TextBlock
HorizontalAlignment="Right"
Grid.Row="3"
Text="{Binding PersonAge }"/>
<TextBlock
HorizontalAlignment="Left"
Grid.Row="4"
Text="Person Address:" />
<TextBlock
HorizontalAlignment="Right"
Grid.Row="4"
Text="{Binding PersonAddress }"/>
<TextBlock
HorizontalAlignment="Left"
Grid.Row="6"
Text="Department:" />
<TextBlock
HorizontalAlignment="Right"
Grid.Row="6"
Text="{Binding PersonDepartmentID}"/>
........
<TextBlock
HorizontalAlignment="Left"
Grid.Row="9"
Text="Eductation :" />
<TextBlock
HorizontalAlignment="Right"
Grid.Row="9"
Text="{Binding PersonLevelEductationID}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
And Commands
namespace Persons.ViewModels
{
class NavigationViewModel : INotifyPropertyChanged
{
public NavigationViewModel()
{
// Set Startup Page
SelectedViewModel = new StartupViewModel();
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
// Select ViewModel
private object _selectedViewModel;
public object SelectedViewModel
{
get => _selectedViewModel;
set { _selectedViewModel = value; OnPropertyChanged("SelectedViewModel"); }
}
public void CrudView()
{
SelectedViewModel = new CrudPage(); // new usercontrol
}
private ICommand _CrudCommand;
public ICommand ShowPageCommand
{
get
{
if (_CrudCommand == null)
{
_CrudCommand = new RelayCommand(param => CrudView());
}
return _CrudCommand;
}
}
// Close App
public void CloseApp(object obj)
{
MainWindow win = obj as MainWindow;
win.Close();
}
// Close App Command
private ICommand _closeCommand;
public ICommand CloseAppCommand
{
get
{
if (_closeCommand == null)
{
_closeCommand = new RelayCommand(p => CloseApp(p));
}
return _closeCommand;
}
}
}
Class RelayCommand
namespace Persons.Utilities
{
public class RelayCommand : ICommand
{
private Action<object> execute;
private Func<object, bool> canExecute;
public RelayCommand(Action<object> execute)
{
this.execute = execute;
canExecute = null;
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return canExecute == null || CanExecute(parameter);
}
public void Execute(object parameter)
{
execute(parameter);
}
}
I apologize for the long code!
22
What you are after is called master-detail view. You can achieve this by using a ContentControl
to display the details view.
You would assign the data model for the details view to the ContentControl.Content
property and define a DatatTemplate
to actually render the view. You assign this template to the ContentControl.Contenttemplate
property or define the DataTemplate
as implicit so that it will be automatically loaded.
Since loading is triggered by a Button
you can pass the details model as command parameter or by binding the ListBox.SelectedItem
property to the view model that also defines the ICommand
.
Some considerations:
ItemsControl
should not be wrapped into aScrollViewer
. Instead wrap theItemsPresenter
of theItemsControl
. This way you properly scroll the items and not theItemsControl
. AListBox
comes with aScrollViewer
by default.ListBox
is an extendedItemsControl
and adds important perfromance features and properties like `SelectedItem that you usually don’t want to miss.- Your
PersonModel
should not access the database to generate a list of its own. Instead, the view model that owns thePersonViewModel
must call the application model to return thePersonModel
collection. The model internally uses the database or reads/writes from/to a file or service. - You don’t have a 1:1 relationship between a control and view model class. In other words, not every control has its own view model. A view model class represents a particular data context. Multiple controls operate with the same data and therefore the same data context. From your data bindings I can tell that the
UserControl
has the wrongDataContext
set. Additionally, you avoid setting the DataContext explicitly and instead let it be inherited. - The
UserControl
should not bind its internals directly to theDataContext
. Instead, introduce dependency properties that the internal can bind to. Then bind these dependency properties to the actualDataContext
. This is especially important if you want to reuse the control in different places or applications. If you don’t need this flexibility, you can of course continue to hardcode the dependencies into your custom controls. I was just pointing out that this is not the best practice. - Try to use composition for your view model classes. For example, you can define a
MainViewModel
that exposes other view models classes like thePersonViewModel
or theNavigationViewModel
. Then assign theMainViewModel
to theDataContext
root (e.g. theMainWindow
). This is far easier to maintain and adds flexibility. For example, such a structure would allow you to use dependency injection, since the only view model to inject would be theMainViewModel
into theMainWindow
. - Creating controls, or handling controls in the view model is violating the MVVM design pattern rules.
- Performing UI logic like opening or closing views in the view model violates the MVVM design pattern. Such code belongs to the view (e.g. code-behind. For example, to close the window or application you can use common event handlers and the
Button.Click
event or use routed commands and theButton.Command
property. - You don’t have to explicitly close resources that implement
IDisposable
. Theusing
-statement will do that for you. Disposing objects will automatically close them and flush buffers e.g. in case ofStream
objects.
MainUserControl.xaml
The master view.
<UserControl>
<!--
Bind to the nested PersonViewModel.
You can also consider to Bind ListBox.SelectedItem to the view model.
-->
<ListBox ItemsSource="{Binding PersonViewModel.PersonItems}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type PersonModel}">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<!-- Bind the Command to the nested NavigationViewModel -->
<RadioButton Content="{Binding Name}"
CommandParameter="{Binding}"
Command="{Binding NavigationViewModel.ShowPageCommand}"
Style="{StaticResource ItemsButtonStyle}" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
PersonModelDetailsView.xaml
The details view.
<UserControl x:Name="Root">
<ContentControl Content="{Binding ElementName=Root, Path=PersonModel}"
ContentTemplate="{Binding ElementName=Root, Path=PersonModelTemplate}" />
</UserControl>
PersonModelDetailsView.xaml.cs
partial class PersonModelDetailsView : UserControl
{
public PersonModel PersonModel
{
get => (PersonModel)GetValue(PersonModelProperty);
set => SetValue(PersonModelProperty, value);
}
public static readonly DependencyProperty PersonModelProperty = DependencyProperty.Register(
nameof(PersonModel),
typeof(PersonModel),
typeof(PersonModelDetailsView),
new FrameworkPropertyMetadata(default));
public DataTemplate PersonModelTemplate
{
get => (DataTemplate)GetValue(PersonModelTemplateProperty);
set => SetValue(PersonModelTemplateProperty, value);
}
public static readonly DependencyProperty PersonModelTemplateProperty = DependencyProperty.Register(
nameof(PersonModelTemplate),
typeof(DataTemplate),
typeof(PersonModelDetailsView),
new FrameworkPropertyMetadata(default));
}
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="PersonModelDetailsViewTemplate"
DataType="{x:Type PersonModel}">
<Border Width="600"
MaxHeight="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Left"
Grid.Row="1"
Grid.Column="0"
Text="Name:" />
<TextBlock HorizontalAlignment="Right"
Grid.Row="1"
Grid.Column="0"
Text="{Binding Name}" />
<TextBlock HorizontalAlignment="Left"
Grid.Row="2"
Text="Surname :" />
<TextBlock HorizontalAlignment="Right"
Grid.Row="2"
Text="{Binding Surname}" />
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Master view -->
<ColumnDefinition /> <!-- Details view -->
<Grid.ColumnDefinitions>
<MainUserControl Grid.Column="0" />
<PersonModelDetailsView Grid.Column="1"
PersonModel="{Binding NavigationViewModel.SelectedPersonModel}"
PersonModelTemplate="{StaticResource PersonModelDetailsViewTemplate}" />
</Grid>
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
private readonly MainViewModel MainViewModel;
public MainWindow()
{
this.MainViewModel = new MainViewModel();
this.DataContext = this.MainViewModel;
InitializeComponent();
// Initialize the view model classes
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Initializing all view models from the loaded event is a significant performnace improvement.
// Just think about doing all of this work from the constructor
// where each view model class instantiates its own classes that
// themself create instances from their constructor and initialize
// them. It would take "ages" for the construction of the top-level
// instance to complete, while the UI remains unresponsive.
// You can even call async methods which isn't possible in the
// constructor.
// Every class must perform expensive/long-running initialization
// outside the constructor.
this.MainViewModel.Initialize();
}
// TODO::Implement window close and application close logic.
// The logic that was originally implemented in the view model class.
// For example use event handlers or routed events or routed commands.
}
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public PersonViewModel PersonViewModel { get; } = new PersonViewModel();
public NavigationViewModel NavigationViewModel { get; } = new NavigationViewModel();
// Can be async if required
public void Initialize()
{
// Do not make such potentially long-running and blocking calls from the constructor
this.PersonViewModel.Initialize();
}
}
PersonViewModel.cs
class PersonViewModel : ViewModelBase
{
public ObservableCollection<PersonModel> PersonItems { get; }
// The model class to access the database
private readonly DataRepository dataRepository;
public PersonViewModel()
{
this.PersonItems = new ObservableCollection<PersonModel>();
}
// Can be async if required
public void Initialize()
{
// Do not make such potentially long-running and blocking calls from the constructor
IEnumerable<PersonModel> personModels = this.dataRepository.GetPersonModels();
foreach (PersonModel personModel in personModels)
{
this.PersonItems.Add(personModel);
}
}
}
NavigationViewModel.cs
class NavigationViewModel : INotifyPropertyChanged
{
public NavigationViewModel()
{
// Set Startup Page
SelectedViewModel = new StartupViewModel();
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
// Select ViewModel
private object _selectedViewModel;
public object SelectedViewModel
{
get => _selectedViewModel;
set { _selectedViewModel = value; OnPropertyChanged(nameof(this.SelectedViewModel)); }
}
// Selected PersonModel
private PersonModel _selectedPersonModel;
public PersonModel SelectedPersonModel
{
get => _selectedPersonModel;
set { _selectedPersonModel = value; OnPropertyChanged(nameof(this.SelectedPersonModel)); }
}
public void ExecuteShowPageCommand(object commandParameter)
{
if (commandParameter is not PersonModel detailsPersonModel)
{
return;
}
// Use data models to navigate.
// Creating controls in the view model class (like you were doing)
// is forbidden in MVVM!
this.SelectedPersonModel = detailsPersonModel;
}
private ICommand _CrudCommand;
public ICommand ShowPageCommand
{
get
{
if (_CrudCommand == null)
{
_CrudCommand = new RelayCommand(ExecuteShowPageCommand);
}
return _CrudCommand;
}
}
/*
* The following code does not belong into a view model class.
* This must be handled by the view! For example in code-behind.
* You can use event handlers for Button.Click events or routed commands.
* Then close the Window or the application from there.
* Handling controls in the view model violates the MVVM design pattern.
*/
//// Close App
//public void CloseApp(object obj)
//{
// MainWindow win = obj as MainWindow;
// win.Close();
//}
//// Close App Command
//private ICommand _closeCommand;
//public ICommand CloseAppCommand
//{
// get
// {
// if (_closeCommand == null)
// {
// _closeCommand = new RelayCommand(p => CloseApp(p));
// }
// return _closeCommand;
// }
//}
}
DataRepository.cs
The model class.
Data persistence (database and file I/O or service consumption) always belongs to the application model. And not the view model or data model classes. Aim to have all queries in a single place (to avoid duplicate code).
class DataRepository
{
public IEnumerable<PersonModel> GetPersonModels()
{
var persons = new List<PersonModel>();
var Connect = ConfigurationManager.ConnectionStrings["MysqlConnection"].ConnectionString;
using (var conn = new MySqlConnection(Connect))
using (var command = new MySqlCommand())
{
conn.Open();
command.Connection = conn;
command.CommandText = @"SELECT person.id, person.name, person.surname, person.address, person.age, person.contact department.name AS 'department', group.name AS 'group', eductation.lavel division.name AS 'division' FROM person
LEFT JOIN department ON department.id = person.departmentID LEFT JOIN group ON group.id = person.groupID LEFT JOIN eductation ON eductation.id = person.eductationLavelID LEFT JOIN division ON division.id = person.divisionID";
using MySqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
Person_list.Add(new PersonModel()
{
PersonID = (int)reader["id"],
Name = (string)reader["name"],
Surname = (string)reader["surname"],
PersonAge = (int)reader["age"],
PersonAddress = (string)reader["address"],
PersonDepartmentID = (string)reader["department"],
PersonGroupID = (string)reader["group"],
PersonContact = (string)reader["contact"],
PersonLevelEductationID = (string)reader["lavel"],
PersonDivisionID = (string)reader["division"]
});
}
}
// Don't close these IDisposables. The using-block will handle that.
//reader. Close();
//conn.Close();
return persons;
}
}