Adding an extra level in wpf mvvm data bound menu item - wpf

I've a menu item named 'Authors' whose ItemsSource is bound to an ObservableCollection called CollectionOfAuthors. Clicking on Authors opens a list of author names. What modifications I need to do to add an extra level of menu item, In simple words, I wish to display list of books for each author-name.
Like this:
Author
|__AuthorName 1
|__Book 1
|__Book 2
Here's my existing code:
MainWindow.xaml
<MenuItem Header="Authors" ItemsSource="{Binding CollectionOfAuthors}">
<MenuItem.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding AuthorName}" />
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
MainWindowViewModel.cs
private ObservableCollection<Author> _collectionOfAuthors;
public ObservableCollection<Author> CollectionOfAuthors
{
get { return _collectionOfAuthors; }
set { SetProperty(ref _collectionOfAuthors, value); }
}
public class Author
{
public string AuthorName {get; set;}
private ObservableCollection<BookDetails> _Books;
public ObservableCollection<BookDetails> Books
{
get { return _Books; }
set { SetProperty(ref _Books, value); }
}
}

Use HierarchicalDataTemplate for this.
<MenuItem Header="Authors" ItemsSource="{Binding CollectionOfAuthors}">
<MenuItem.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Books}">
<TextBlock Text="{Binding AuthorName}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding BookName}"/>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>

Related

Bind ListViewItem ContextMenu MenuItem Command to ViewModel of the ListView's ItemsSource

I have a ListView of Expanders. Each Expander (representing a database table) will have items under it, in another ListView. I want to Right-Click and have an "Edit" option on the innermost items, which represent records in the corresponding database table.
There is an ICommand named 'Edit' in my MainEditorViewModel. The Datacontext in which this command resides is the same as that of the outermost ListView named "TestGroupsListView"
Here is the XAML markup for the ListView of Expanders. The outermost ListView I've named for referencing in the binding via ElementName for the MenuItem's Binding:
<ListView Name="TestGroupsListView" ItemsSource="{Binding TestGroups}" Grid.Row="1">
<ListView.ItemTemplate>
<DataTemplate>
<Expander Style="{StaticResource MaterialDesignExpander}" >
<Expander.Header>
<Grid MaxHeight="50">
<TextBlock Text="{Binding Name}"/>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Add..." Command="{Binding Add}"/>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Expander.Header>
<ListView ItemsSource="{Binding Records}" Style="{StaticResource MaterialDesignListView}" Margin="30 0 0 0">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit"
Command="{Binding ElementName=TestGroupsListView, Path=DataContext.Edit}"
CommandParameter="{Binding }"/>
</ContextMenu>
</Grid.ContextMenu>
<Button Content="{Binding RecordName}" Command="{Binding ElementName=TestGroupsListView, Path=DataContext.Edit}"/>
<!--<TextBlock Text="{Binding RecordName}" AllowDrop="True"/>-->
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I am able to bind a button in the DataTemplate to 'Edit' successfully, but when I attempt to bind the MenuItem's Command to 'Edit', nothing happens. Why might this be that the button command binding works using ElementName but the same binding in the ContextMenu doesn't?
I think it will be better to use the context menu globally for ListView and globally for each Child ListView. Ok, here is my solution:
<ListBox ItemsSource="{Binding Groups}">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Add..." Command="{Binding Add}"/>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Name}">
<ListView ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit" Command="{Binding Edit}" IsEnabled="{Binding CanEdit}"/>
</ContextMenu>
</ListView.ContextMenu>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And for better understanding code behind:
public class GroupsVM : ViewModelBase
{
public ICommand Add
{
get => null; //Command implementation
}
public ObservableCollection<GroupVM> Groups { get; set; } = new ObservableCollection<GroupVM>()
{
new GroupVM { Name = "First" },
new GroupVM { Name = "Second" },
new GroupVM { Name = "Third" }
};
}
public class GroupVM : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
public ICommand Edit
{
get => null; //Command implementation
}
public bool CanEdit => SelectedRecord != null;
public ObservableCollection<RecordVM> Records { get; set; } = new ObservableCollection<RecordVM>()
{
new RecordVM { Name="Record1" },
new RecordVM { Name="Record2" },
new RecordVM { Name="Record3" }
};
private RecordVM _selectedRecord = null;
public RecordVM SelectedRecord
{
get => _selectedRecord;
set
{
_selectedRecord = value;
OnPropertyChanged();
OnPropertyChanged("CanEdit");
}
}
}
public class RecordVM : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
}

Checkbox not showing as checked inside ListView

I have a checkbox inside a listview. The listview is bound to an observable collection. When I use the context menu to try to select all Checkboxes, they do not show as checked. What am I doing wrong?
<ListView Grid.Row="1" Grid.Column="0" ItemsSource="{Binding AvailableModels}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
SelectionMode="Extended">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Select All Models" Command="{Binding
SelectAllModelsAction}" />
<MenuItem Header="Deselect All Models" Command="{Binding
DeselectAllModelsAction}" />
</ContextMenu>
</ListView.ContextMenu>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding DataContext.IsSelected,
RelativeSource={RelativeSource
FindAncestor, AncestorType={x:Type
ListViewItem}}}" VerticalAlignment="Center" />
<Label Content="{Binding Name}" Margin="2,0,0,0" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ViewModel
public ObservableCollection<ListItems> AvailableModels
{
get
{
return this.availableModels;
}
set
{
this.availableModels = value;
this.NotifyPropertyChanged(m => m.AvailableModels);
}
}
Context Menu Action
private void SelectAllModels()
{
foreach (var model in this.AvailableModels)
{
model.IsSelected = true;
}
this.NotifyPropertyChanged(m => m.AvailableModels);
}
ListItems Object
public class ListItems
{
public string Name
{
get;
set;
}
public object Value
{
get;
set;
}
public bool IsSelected
{
get;
set;
}
}
ListItems isn't implementing INotifyPropertyChanged so changing IsSelected isn't updating the UI.
Calling NotifyPropertyChanged in SelectAllModels() is unnecessary since the collection itself isn't changed. The NotifyProperyChanged() call in the AvailableModels setter updates the UI when a new collection is set. And ObservableCollection will handle when the collection is modified (items added/removed). However changes to the properties within the ListItems does not update the UI unless they call NotifyPropertyChanged in the setters.

WPF ListView grouping by 2 columns but display only 1 group header

A ListView displays a collection of the following class:
public class Employee
{
private string _department;
private string _manager;
private string _name;
private string _address;
public string Department
{
get { return _department; }
}
public string Manager
{
get { return _manager; }
}
public string Name
{
get { return _name; }
}
public string Address
{
get { return _address; }
}
}
There is a 1-to-1 relation between Department and Manager, so any 2 rows with the same department will also have the same manager.
I want to group by Department/Manager, with the group header showing "Department (Manager)".
My CollectionViewSource looks like
<CollectionViewSource x:Key="cvsEmployees" Source="{Binding Employees}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Department" />
<PropertyGroupDescription PropertyName="Manager" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
The plan is to not display the first level header (Department) and to somehow bind to both the Department (1st level) and the Manager (2nd level) from the 2nd level header.
3 questions:
To avoid displaying the 1st level header, I have an empty data template in the groupstyle:
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
This seems very clunky. Is there a more elegant way to skip a group header?
How do I bind to the 1st grouping level property (Department) from the 2nd level header (Manager) to achieve the required "Department (Manager)" ?
Is there a better way to do this than creating 2 grouping level?
Thanks
Solved the main stumbling block, question 2 above: how to bind from the group header to a property that is not the grouping property.
The solution is to change the data context to:{Binding Items}. The ItemSource properties are then available
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,10,0,3" DataContext="{Binding Items}" >
<TextBlock Text="{Binding Path=Department}" FontWeight="Bold" Margin="3"/>
<TextBlock Text="{Binding Path=Manager, StringFormat='({0})'}" Margin="3"/>
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
I would create another model part, which represents the dual grouping that you need to have happen:
Model Classes:
public class EmployeeModel {
private readonly Employee _Employee;
public DepartmentManager ManagementInfo { get; private set; }
public string Name {
get { return _Employee.Name; }
}
public string Address {
get { return _Employee.Address; }
}
public EmployeeModel(Employee employee) {
this._Employee = employee;
this.ManagementInfo = new DepartmentManager(employee.Department, employee.Manager);
}
}
public class DepartmentManager {
public string Department { get; private set; }
public string Manager { get; private set; }
public DepartmentManager(string dept, string manager) {
this.Department = dept;
this.Manager= manager;
}
public override bool Equals(object obj) {
var model = obj as DepartmentManager;
if(null == model)
return false;
return Department.Equals(model.Department, StringComparison.InvariantCultureIgnoreCase) &&
Manager.Equals(model.Manager, StringComparison.InvariantCultureIgnoreCase);
}
}
XAML:
<CollectionViewSource x:Key="cvsEmpsModel" Source="{Binding EmployeesModel}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="ManagementInfo" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<DataTemplate DataType="{x:Type models:EmployeeModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</DataTemplate>
...
<ListView ItemsSource="{Binding Source={StaticResource cvsEmpsModel}}">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,10,0,3" DataContext="{Binding Items}">
<TextBlock Text="{Binding Path=ManagementInfo.Manager}" FontWeight="Bold" Margin="3" />
<TextBlock Text="{Binding Path=ManagementInfo.Department, StringFormat='({0})'}" Margin="3" />
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
Then in your Window/ViewModel:
this.EmployeesModel = new ObservableCollection<EmployeeModel>(MyListOfEmployersFromDB.Select(e => new EmployeeModel(e)));
Note, I've overriden Equals in the DepartmentManager class, but not GetHashCode, ideally you should do a custom implementation of that. I had to override equals so the grouping view source would correctly group the same entries. You could get rid of this need, buy constructing the DepartmentManager for the same Employees outside of the collection, and pass them into the EmployeeModel ctr.

Using Commands in WPF TreeView with HierarchicalDataTemplate

is there a way to use commands in a treeview with a HierarchicalDataTemplate, so that i can react on the click on an item in the treeview?
i would prefer a solution without code-behind if there is one.
Here is my TreeView:
<TreeView ItemsSource="{Binding Main.TreeItems}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
<TextBlock Text="{Binding Path=Header}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
provide an property of type ICommand on your Item (ViewModel) and Bind to that. So your item class would be something like:
class MyTreeItem
{
public MyTreeItem()
{
this.SomeCommand = /* create command here */ null;
this.Children = new ObservableCollection<MyTreeItem>();
}
public ICommand SomeCommand { get; private set; }
public ObservableCollection<MyTreeItem> Children { get; private set; }
}
In xaml you can then write:
<TreeView ItemsSource="{Binding Main.TreeItems}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
<TextBlock Text="{TemplateBinding Header}" />
<Button Text="My Command" Command="{TemplateBinding SomeCommand}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
I think that should work, at least it did similarily last time i used that :)

How to read all textbox values(Map to database fileds) and store them in Database in WPF - MVVM

I am new to WPF and MVVM. After a long invention i got to know how to retrieve data from database and bind it to a itemcontrol/listview/gridview.
But my problem i am not getting how to read bunch of textbox values and store as a new record in database.
Here is my sample code..
View
<ItemsControl ItemsSource="{Binding AllEmployees}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox Width="100" Text="{Binding FirstName}" Margin="4"/>
<TextBox Width="100" Text="{Binding LastName}" Margin="4"/>
<TextBox Width="100" Text="{Binding Age}" Margin="4"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- For new Employee Details -->
<StackPanel>
<TextBox x:Name="FirstName"/>
<TextBox x:Name="LastName"/>
<TextBox x:Name="Age"/>
<Button Content="New" Command="{Binding NewEmployeeCommand}"/>
</StackPanel>
My cs file is
public ObservableCollection<DataAccess.Employee> AllEmployees
{
get;
private set;
}
public EmployeeListViewModel(EmployeeRepository employeeRepository)
{
if (employeeRepository == null)
{
throw new ArgumentNullException("employeeRepository");
}
_employeeRepository = employeeRepository;
this.AllEmployees = new ObservableCollection<DataAccess.Employee>
(_employeeRepository.ListAll());
}
Now how could i store a new employee Firstname, Lastname, Age in database by reading those text boxes..
How to write function for NewEmployeeCommand event to read the textboxes( mapping of textboxes to appropriate datafileds in database) and store the data in database.
Thanks a Lot !
if you're trying to use MVVM just need to:
Create your ViewModel to contain all the properties your View needs
Bind to those properties in Xaml
For example:
public class EmployeeListViewModel
{
public ObservableCollection<Employee> AllEmployees {get;private set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public int? Age {get;set;}
public ICommand NewEmployeeCommand {get;set;}
//You need to connect to this method by using a Delegate/RelayCommand see link below
public void AddNewEmployee()
{
//Add your real code here to actually insert into the db
var result = InsertEmployeeIntoDatabase(FirstName,LastName,Age);
//You probably want to add this new employee to the list now ;)
AllEmployees.Add(result);
//Now you probably want to reset your fields
FirstName = null;
LastName = null;
Age = null;
}
}
Click here for an implementation of a delegate command
And then just edit your xaml like this:
<ItemsControl ItemsSource="{Binding AllEmployees}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox Width="100" Text="{Binding FirstName}" Margin="4"/>
<TextBox Width="100" Text="{Binding LastName}" Margin="4"/>
<TextBox Width="100" Text="{Binding Age}" Margin="4"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- For new Employee Details -->
<StackPanel>
<TextBox Text={Binding FirstName}"/>
<TextBox Text={Binding LastName}"/>
<TextBox Text={Binding Age}"/>
<Button Content="New" Command="{Binding NewEmployeeCommand}"/>
</StackPanel>
You could pass references in the command parameter:
<StackPanel>
<TextBox x:Name="FirstName"/>
<TextBox x:Name="LastName"/>
<Button Content="New" Command="{Binding NewEmployeeCommand}">
<Button.CommandParameter>
<x:Array Type="{x:Type TextBox}">
<x:Reference Name="FirstName"/>
<x:Reference Name="LastName"/>
</x:Array>
</Button.CommandParameter>
</Button>
</StackPanel>
Depending on what kind of event you use you case the parameter and get the values:
TextBox[] textBoxes = e.Parameter as TextBox[]; //RoutedEvent
TextBox[] textBoxes = parameter as TextBox[]; //If the executed handler provides the parameter
string firstName = textBoxes[0].Text;
string lastName = textBoxes[1].Text;
//create entry; store in DB
Via binding:
<Button.CommandParameter>
<local:MyEntry FirstName="{Binding ElementName=FirstName, Path=Text}"
LastName="{Binding ElementName=LastName, Path=Text}"/>
</Button.CommandParameter>
MyEntry entry = parameter as MyEntry;
//store in DB
you dont read the Textbox values. you need a NewEmployeeViewModel and bind the TextBoxes to the properties.
EDIT:
just create a class with INotifyPropertyChanged and the Properties you need.
public class NewEmployee : INotifyPropertyChanged
{
public string FirstName
{
get{return this._firstname;}
set{this._firstname = value;
OnPropertyChanged("FirstName");}
}
//... other properties
}
xaml
<StackPanel DataContext={Binding MyNewEmployeeProperty}>
<TextBox x:Name="FirstName" Text={Binding FirstName}/>
<TextBox x:Name="LastName" Text={Binding LastName}/>
<TextBox x:Name="Age" Text={Binding Age}/>
<Button Content="New" Command="{Binding NewEmployeeCommand}"/>
</StackPanel>
I got the Correct answer.
My New entry form in xmal Should be
<StackPanel>
<TextBox Text={Binding Employee.FirstName}"/>
<TextBox Text={Binding Employee.LastName}"/>
<TextBox Text={Binding Employee.Age}"/>
<Button Content="New" Command="{Binding NewEmployeeCommand}"/>
</StackPanel>
My cs file
public class EmployeeListViewModel : INotifyPropertyChanged
{
Employee _employee;
RelayCommand _addNewEmployee;
EmployeeRepository _employeeRepository;
public Employee Employee
{
get
{
return _employee;
}
set
{
_employee = value;
OnPropertyChanged("Employee");
}
}
public void NewEmployeeCommand()
{
if(_addNewEmployee == null)
{
_addNewEmployee = new RelayCommand( param => NewEmployeeCommandExecute(),
param => NewEmployeeCommandCanExecute
);
}
}
void NewEmployeeCommandExecute()
{
_employeeRepository.Add(Employee);
_employeeRepository.Save();
}
bool NewEmployeeCommandCanExecute
{
get
{
return true;
}
}
}

Resources