Filling Contextmenu on demand - wpf

I have an entity view model. As there can be many entities in the UI, I have used a DataTemplate for representing an entity view model. This data template is used by the container control's ItemTemplate property to render the entities.
Please note this is not the conventional ListView/ListBox control. It is a Graph control with the edges and nodes being represented by the data templates.
This said, each of the nodes are of different type and when the user right clicks, the context menu of the template is getting bound to a collection provided by the view model. Following code is working fine:
<DataTemplate DataType="{x:Type model:Person}">
<Border Style="{StaticResource NodeBorderStyle}" MinWidth="200">
<Border.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextOperations}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding PlacementTarget.DataContext.HandleContextOperationCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Setter Property="CommandParameter" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Header}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Border.ContextMenu>
....................
</Border >
</DataTemplate>
This has a limitation, the ContextOperations (an ObservableCollection) property has to be populated when the view model gets initialized.
There may be numerous nodes and hence storing context menu items for all the nodes at loading time can be a matter of huge memory.
I want that when the user right clicks the entity (the template), the ContextOperations property of the viewmodel gets populated and then the context menu gets rendered.
This DataTemplate is kept in a resource file.
Any guidance would be appreciated.
Thank you.
regards,

Handle the MouseRightButtonUp event on you datatemplate border and create the context menu runtime like this:
private void borderMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
OpenContextMenu(sender as ComboBox);
}
private void OpenContextMenu(FrameworkElement element)
{
if (element.ContextMenu == null)
{
ContextMenu c = new ContextMenu();
//Load the ContextOperations from ViewModel
//based on the Framework element datacontext
ContextOperations.ToList().ForEach(co => c.Items.Add(new MenuItem()
{
//Create your menu item
}));
element.ContextMenu = c;
}
element.ContextMenu.PlacementTarget = element;
element.ContextMenu.IsOpen = true;
}
and remove the definition of contextmenu from the XAML.

Related

WPF ContextMenu bound to 3 Listboxes on right-click

I have three tabs and each has a listbox with different types of files.
When I right-click on an item in the listbox, I want a ContextMenu with "New, Edit and Delete" as Item headers.
I guess I could have a ContextMenu for each listbox, and then have a seperate method for each header, such as:
<ListBox.ContextMenu>
<ContextMenu x:Name="NewEditDeleteAdvCalcFileContextMenu">
<MenuItem Name="NewAdv" Header="New" Click="NewAdv_Click" />
<MenuItem Name="EditAdv" Header="Edit" Click="EditAdv_Click"/>
<MenuItem Name="DeleteAdv" Header="Delete" Click="DeleteAdv_Click"/>
</ContextMenu>
</ListBox.ContextMenu>
But really, I hope there is a better way.
I saw this post which shows the ContextMenu as Static Resource
and this seems to be something I would like to do.
In the same thread it is suggested to use commands:
ContextMenu with Commands
and with that I'm hoping I can get the type of the ListBoxItem that was clicked, because I need that. A new file type B must be handled differently than a new file type C, but I don't want a gazillion contextmenus and New/Edit/Delete methods.
So, currently I have this higher up in my xaml file:
<UserControl.Resources>
<ContextMenu x:Key="NewEditDeleteContextMenu">
<MenuItem Header="New"
Command="{Binding Path=NewFileCommand}"
CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"/>
<MenuItem Header="Edit"
Command="{Binding Path=EditFileCommand}"
CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"/>
<MenuItem Header="Delete"
Command="{Binding Path=DeleteFileCommand}"
CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"/>
</ContextMenu>
</UserControl.Resources>
And then a listbox in the tabItem:
<ListBox Name="CalcFilesListBox"
Margin="20" ItemsSource="{Binding CalcFilesList}"
PreviewMouseRightButtonUp="ListBox_PreviewMouseRightButtonUp"
ContextMenu="{StaticResource NewEditDeleteContextMenu}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<EventSetter Event="MouseDoubleClick" Handler="CalcFileListBox_MouseDoubleClick"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Question #1
How do I get the rightclick of a ListBoxItem to show the ContextMenu, which is now a static resource?
Because in my xaml.cs I had this:
private void ListBox_PreviewMouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
// SelectItemOnRightClick(e);
NewEditDeleteContextMenu.PlacementTarget = sender as UIElement;
NewEditDeleteContextMenu.IsOpen = true;
}
But now I have an error saying:
The name 'NewEditDeleteContextMenu' does not exist in the current context.
because originally I had the contextmenu as part of the ListBox such as:
<ListBox.ContextMenu>
...
But as far as I could see that would mean a separate ContextMenu for each ListBox.
Question #2
Is the correct way to use a command, let's say NewFileCommand for the New item header in the ContextMenu (shown in the UserControl.Resources block of code) to do the following:
In my ViewModel:
public RelayCommand<string> NewFileCommand { get; private set; }
and then in the ViewModel's constructor:
public CalcViewModel()
{
NewFileCommand = new RelayCommand<object>(NewFile);
}
public void NewFile(object sender)
{
//Determine the type of file, based on the ListBoxItem's DataContext.
That is, supposing the ListBoxItem is the object being passed as the sender.
}
Basically, I want one ContextMenu bound to the different ListBox components, and this should pop up on a rightclick, and when for instance the New item is chosen on the ContextMenu, I want to determine the type of the file that has been bound to the ListBox.
E.g.: ListBox 1 is bound to a collection of file type B. ListBox 2 is bound to a collection of file type C. When I rightclick on an item in ListBox 2, and choose New, I need to make a new file of type C.
Question #3
This isn't a very intricate View. I haven't used a MVVM framework because so far I haven't thought that the time it would take me to learn one would be worth it, but considering this scenario, and a simpler case for a double-click on the ListBoxItems that can be seen in one of the blocks of code, would you recommend the use of a framework?
You're going in the right direction, you code just needs a bit of updating. First, don't need any right-click handlers -- if a control has a ContextMenu set, right-clicking will invoke that ContextMenu. Having a ContextMenu as a StaticResource and attaching it to multiple controls creates a bit of a problem because of a bug in .NET where a ContextMenu doesn't update its DataContext after initially setting it. That means if you first invoke the menu on listbox #2, you'll get the selected item in that listbox... but if you then invoke it on listbox #3, you'll still get the selected item in listbox #2. But there's a way around this.
First, let's look at the context menu and how it's bound to a list box:
<ContextMenu x:Key="contextMenu" DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="New" Command="{Binding DataContext.NewFileCommand}" CommandParameter="{Binding}"/>
<MenuItem Header="Delete" Command="{Binding DataContext.DeleteFileCommand}" CommandParameter="{Binding SelectedItem}"/>
</ContextMenu>
...
<ListBox Margin="10" ItemsSource="{Binding Files1}" ContextMenu="{StaticResource contextMenu}"/>
PlacementTarget is the control the ContextMenu is attached to. Explicitly binding the menu's data context to PlacementTarget ensures it's pointing to the correct ListBox every time it's invoked. Commands like "Edit" and "Delete" that deal with list items are then easy: Just bind the CommandParameter (not the CommandTarget as you did) to the ListBox's SelectedItem. The item you want to edit or delete will then be given as a parameter to the command.
Since you used RelayCommand I'm assuming you used GalaSoft's MVVM framework. In that case here's how your "Delete" command might look:
public RelayCommand<object> DeleteFileCommand { get; } = new RelayCommand<object>( DeleteFile_Executed, DeleteFile_CanExecute );
private static bool DeleteFile_CanExecute( object file )
{
return file != null;
}
private static void DeleteFile_Executed( object file )
{
var filetype = file.GetType();
System.Diagnostics.Debug.WriteLine( string.Format( "Deleting file {0} of type {1}", file, file.GetType() ) );
// if( filetype == typeof( FileTypeA ) ) DeleteFileTypeA( file as FileTypeA );
// else if( filetype == typeof( FileTypeB ) ) DeleteFileTypeB( file as FileTypeB );
// etc...
}
The "New" command will be a bit tricker because you want to be able to create a new item whether an item is selected or not. So we'll bind the CommandParameter to the ListBox itself. Unfortunately there's not a good way to get the type of item the ListBox contains. It could contain multiple types of items, or no items at all. You could give it an x:Name then look at the name in your command handler, but what I choose to do is put the type of item this ListBox handles as the Tag parameter of the ListBox. Tag is a bit of extra data you can use for whatever purpose you like:
<ListBox Margin="10" ItemsSource="{Binding Files1}" ContextMenu="{StaticResource contextMenu}" Tag="{x:Type local:FileTypeA}"/>
Now we can define our "New" command handlers like this:
private static bool NewFile_CanExecute( ListBox listbox ) { return true; }
private static void NewFile_Executed( ListBox listbox )
{
var filetype = listbox.Tag as Type;
System.Diagnostics.Debug.WriteLine( string.Format( "Creating new file of type {0}", filetype ) );
// if( filetype == typeof( FileTypeA ) ) CreateNewFileTypeA();
// else if( filetype == typeof( FileTypeB ) ) CreateNewFileTypeB();
// etc...
}
As for whether this scenario warrants an MVVM or not, you can certainly put your three file lists in a ViewModel, along with code that actually creates, edits, and deletes the files, and have your commands in the Window invoke the code in the ViewModel. I usually don't, though, until the scenario becomes more complicated.

How to refresh treeview when ItemsSource changes?

My TreeView is bound to an ObservableCollection and uses HierarchicalDataTemplates.
I get the models from a web service.
Only when users click a node in tree, a web service call will be sent to get its child items.
My App has a TabControl, TreeView is on one tabpage, the other tabpage has a datagrid - it has some data selected from treeview. When an item in datagrid is right clicked, I want to locate the item on treeview. But now the issue is when I iterate the treeview,
say, I have an item called
A.A1.A11
and my TreeView has 3 items in the first level:
A
B
C
when I locate A.A1.A11, I want to expand A, A1, and highlight A11.
When I iterate the treeview, first I find A, it matches first path of A.A1.A11,
so I send a web service request to get A's children.
Once I get that, DataContext of the TreeViewItem of A is updated, but the TreeViewItem itself is not.
So when I check A.Items, it is empty and iteratation is unable to continue.
How can I refresh the TreeView & TreeViewItem when its ItemsSource changes?
Here is the xaml for treeview definition
<TreeView x:Name="TreeRoot"
ItemsSource="{Binding RootItems}"
TreeViewItem.Expanded="TreeViewExpanded"
TreeView.SelectedItemChanged="TreeRootSelectedItemChanged"
ContextMenu="{StaticResource DataGridContextMenu}"
PreviewKeyDown="TreeViewKeyDown"
PreviewMouseLeftButtonDown="TreeViewPreviewLeftMouseButtonDown"
PreviewMouseMove="TreeViewPreviewMouseMove" >
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type m:TreeModelBase}"
ItemsSource="{Binding Children}">
<StackPanel>
<Image Source="{Binding ImageFilePath}" />
<TextBlock Text="{Binding Name}" />
<!-- other items removed for brevity -->
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}"
Value="True">
<Setter Property="Background" Value="DodgerBlue"/>
<Setter Property="TextBlock.Foreground"
Value="White"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}"
Value="False">
<Setter Property="Background" Value="White"/>
<Setter Property="TextBlock.Foreground"
Value="Black"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Does RootItems implement INotifyProperty changed and are you calling NotifyPropertyChanged? Collections need to be an ObservableCollection (not a List) for the UI to know about updates.
When populating TreeViewItem's children, you don't update DataContext, you update ItemsSource. So after the service gets the children for A, you need to assign the resulting List (or ObservableCollection) to A.ItemsSource.
In case you're using DataContext and proper data binding technique, update of the TreeViewItem can be achieved by connecting PropertyChanged to each item and deriving TreeViewItem like mentioned below (resetting DataContext and template of the tree item).
This example refreshes only 2nd level items (second condition), might be easily extended of course.
In my case ElementItem is the base class for every item in ObservableList and implements INotifyProperty.
...
itemInObservableList.PropertyChanged += Ep_PropertyChanged;
...
private void Ep_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var twi = this.pageList.ItemContainerGenerator.ContainerFromItem(
(sender as ElementItem).Parent ) as TreeViewItem;
if (twi != null)
{
var twii = twi.ItemContainerGenerator.ContainerFromItem(sender) as TreeViewItem;
if (twii != null)
{
//second level items refresh
var dc = twii.DataContext;
var tpl = twii.Template;
twii.DataContext = null;
twii.Template = null;
twii.DataContext = sender ;
twii.Template = tpl;
twii.UpdateLayout();
}
}
}
I found & managed to get it work. but it is really not optimal at all. What I did, iterate viewModel first, send web service request to get A's children, then send another request to get A1's children. This is the first pass. Second, search treeview from root and generate ItemContainers while iteration. First, search root of tree, get node A (treeviewItem), then apply template, get container from ItemContainerGenerator (see detail # How to refresh treeview when ItemsSource changes?), then I am able to find the item on treeview and expand its path. Anyone knows a better way, let me know. thanks

Silverlight how to promote selected item of listbox when using buttons within datatemplates

Within a Listbox control I have a Data Template which consists of text and a button. Given the nature of Silverlight/WPF when I click on the button within the listbox item the button event is trapped before the listbox item is selected. Therefore if I am trying to pass the record ID of the selected listbox item I am currently only able to do so by first clicking and selecting the listbox item and then clicking on the button.
Is there a way to promote the selection of the listbox item so that when the listbox items are created I have the ability to click on the button within the listbox item and some event (selectionChanged ?) is invoked which would allow me to capture the selected record id and use it for some other action ( pass as a parameter in a method etc). I'm using Simple MVVM toolkit for this implementation so I was wondering if this could be handled in the viewModel or if I would need to handle this in the controls code behind and then push the selection to the viewModel.
The listbox control is presented as:
<ListBox x:Name="ResultListBox"
HorizontalAlignment="Stretch"
Background="{x:Null}"
Grid.Row="1"
BorderThickness="0" HorizontalContentAlignment="Stretch"
ItemContainerStyle="{StaticResource ListBoxItemStyle1}"
ItemsSource="{Binding SearchResults[0].Results}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
Style="{StaticResource ListBoxStyle1}">
<ListBox.ItemTemplate>
<DataTemplate>
<dts:TypeTemplateSelector Content="{Binding}" HorizontalContentAlignment="Stretch">
<!-- Template 1 -->
<formatter:TypeTemplateSelector.CFSTemplate>
<DataTemplate>
<qr:ucIndex_Product />
</DataTemplate>
</formatter:TypeTemplateSelector.CFSTemplate>
<!-- Template 2 -->
<formatter:TypeTemplateSelector.PersonTemplate>
<DataTemplate>
<qr:ucIndex_Person />
</DataTemplate>
</formatter:TypeTemplateSelector.PersonTemplate>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Within the datatemplate (user control) resides the button along with a number of other fields. I'll omit that code for the time being unless requested.
Thanks in advance!
Put this in your ListBox.Resources
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="PreviewGotKeyboardFocus" Handler="SelectCurrentItem"/>
</Style>
And this in the Code Behind
protected void SelectCurrentItem(object sender, KeyboardFocusChangedEventArgs e)
{
ListBoxItem item = (ListBoxItem)sender;
item.IsSelected = true;
}
You could use the following code as well which doesn't use code-behind, however it only keeps the ListBoxItem selected for as long as it has KeyBoard focus. Once focus leaves, the item becomes unselected
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
EDIT
Since Silverlight doesn't have EventSetters, you can use the ListBox's Loaded event and add the following to your code behind:
private void ResultListBox_Loaded(object sender, RoutedEventArgs e)
{
ListBox list = (ListBox)sender;
list.GotFocus += ResultListBox_GotFocus;
}
void ResultListBox_GotFocus(object sender, RoutedEventArgs e)
{
var item = FindAncester<ListBoxItem>((DependencyObject)e.OriginalSource);
if (item != null) item.IsSelected = true;
}
T FindAncester<T>(DependencyObject current)
where T : DependencyObject
{
current = VisualTreeHelper.GetParent(current);
while (current != null)
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
};
return null;
}
This captures the Focus event for the ListBox, takes the control that triggered the focus event and traverses up the visual tree to find the ListBoxItem objects, and sets it's Selected value to true.
Rachel's solution works great. The one issue I did find in this approach was that it does place total focus on the selected item. As a result the user would be required to double click within the control to place focus on other items such as selectable text or other button. After working with this a bit more I discovered you can also resolve this by setting the listbox selected items to the data context of the object you are clicking on etc. This works well as allows you to set this to any UI object within the control.
ListBox.SelectedItem = ((HyperlinkButton)sender).DataContext;
In this example I had Hyperlink buttons within the data template. Clicking on them would then set the focus to the selected listbox item.
rlcrews got it right! Use DataContext:
ObservableCollection<Employee> employees1;
...
listBox1.ItemsSource = employees1;
...
//DataTemplate in ListBox has a button with following event
private void bnPromoteEmployee_Click(object sender, RoutedEventArgs e)
{
Employee emp1 = (Employee)((Button)sender).DataContext;
emp1.Promote();
}

Silverlight relativebinding ItemTemplate ListboxItem - Listbox

I created programatically a class (I called it ViewGrid) so that I use an instance of it as ItemTemplate for my ListBox control; of course, it's my data template for the listboxitem....
Also, in my ViewGrid class, I got a dependency property called IsChecked and I want to keep it in sync with the ListBoxItem's IsSelected property. I noticed that in SL there no relativesource-findancestor-ancestortype support for binding as in WPF, still, I need to find a way to keep my IsChecked property synchronized with the IsSelected property of the internally generated ListBoxItem for my ListBox control. Can you help?
Here is a ListBox defined in XAML that uses the IsSelected property of each LitBoxItem to show or hide a button when selected. You just need to duplicate that Binding approach for the ListBoxItems you create in code. Either that, or create a UserControl with the appropriate ListBoxItem XAML, and insert instances of those UserControls into your ListBox.
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Width="200" Height="120">
<StackPanel Margin="5">
<TextBlock Text="{Binding Name, Mode=OneWay}" />
<StackPanel Visibility="{Binding IsSelected, Mode=OneWay, Converter={StaticResource BoolToVisible}}">
<Button Content="Show Details" Click="OnDetailsClick" Tag="{Binding}" />
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Good luck,
Jim McCurdy
Face To Face Software and YinYangMoney
UPDATE: I revisited this and found a much better solution. My original one remains below, but the way I actually ended up solving this problem is via using the ViewGrid in a ControlTemplate instead of a DataTemplate. Then you can use the RelativeSource TemplatedParent binding to bind to the IsSelected property of the ListBox. So, add the following to the Resources of the listbox or your page or user control:
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<StackPanel>
<ViewGrid IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>​
<!-- other controls may go here -->
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
ORIGINAL:
So after seven years, you almost certainly don't need an answer to this anymore... however, I recently spent a morning wrestling with this issue and thought I'd give my solution in case any similar unfortunate ends up here.
First off, anyone who's using Silverlight 5 is in luck as AncestorType is apparently now available for RelativeSource, letting you bind directly to the IsSelected property of the ListBoxItem. For those of us stuck with 4 or below, the only real workaround I came up with was "faking" the binding via use of events in the code behind.
To do this, assume you have your YourView XAML with a ListBox named "lbYourListBox" which has its ItemsSource and SelectedItem properties bound to appropriate properties on a YourViewModel class, along with a ViewGrid in its ItemTemplate whose IsChecked property is not bound to anything. Then, in your code behind file, you wire up events as follows:
public YourView()
{
InitializeComponent();
this.Loaded += (sender, e) =>
{
((YourViewModel)this.DataContext).PropertyChanged += vm_PropertyChanged;
UpdateViewGrids();
};
}
// this part propagates changes from the view to the view model
private void viewGrid_Checked(object sender, RoutedEventArgs e)
{
var selectedVM = ((ViewGrid)sender).DataContext as SourceItemType;
((YourViewModel)this.DataContext).SelectedViewGridItem = selectedVM;
}
private void vm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (string.Equals(e.PropertyName, "SelectedViewGridItem"))
{
UpdateViewGrids();
}
}
// this part propagates changes from the view model to the view
private void UpdateViewGrids()
{
var viewGrids = this.lbYourListBox.GetVisualDescendants().OfType<ViewGrid>();
var selectedVM = ((YourViewModel)this.DataContext).SelectedViewGridItem;
foreach (var grid in viewGrids)
{
grid.IsChecked = selectedVM == grid.DataContext;
}
}​
The viewGrid_Checked event handler should be wired up to the Checked event of the view grid in the ItemTemplate. The GetVisualDescendants() method comes from the Silverlight Toolkit.
Important caveats:
The ViewGrid.Checked event should not fire except for the unchecked->checked transition, and no more than one view grid should be able to be selected at once. If those two things aren't true, you'll have to make appropriate edits to ensure this code can't cause an infinite event-driven loop. (Of course, if you don't need two-way binding, you only need one of these event handlers and event ping-pong isn't a concern.)
I wrote this for a user control which had its data context set in XAML, which is why the event handler for the view model's PropertyChanged event is only assigned after the view is loaded. Depending on how and when your view and view model are bound to each other, you may have to assign that earlier/later/differently.
This won't work if the view grids aren't visible, GetVisualDescendants seems to ignore hidden/collapsed controls.

What is the best way in MVVM to build a menu that displays various pages?

I want to build a simple application with the MVVM pattern.
This application will have two main parts:
menu on top
content below
The navigation will be simple:
each menu item (e.g. "Manage Customers" or "View Reports") will fill the content area with a new page that has some particular functionality
I have done this before with code behind where the code-behind event-handler for menu items had all pages loaded and the one that should be displayed was loaded in as a child of a StackPanel. This, however, will not work in MVVM since you don't want to be manually filling a StackPanel but displaying e.g. a "PageItem" object with a DataTemplate, etc.
So those of you who have made a simple click-menu application like this with MVVM, what was your basic application structure? I'm thinking along these lines:
MainView.xaml:
<DockPanel LastChildFill="False">
<Menu
ItemsSource="{Binding PageItemsMainMenu}"
ItemTemplate="{StaticResource MainMenuStyle}"/>
<ContentControl
Content="{Binding SelectedPageItem}"/>
</DockPanel>
where the Menu is filled with a collection of "PageItems" and the DataTemplate displays the Title of each "PageItem object" as the Header of each MenuItem.
And the ContentControl will be filled with a View/ViewModel pair which has full functionality, but am not sure on this.
First, I think you should keep the code-behind event handler, there's no point in changing a simple 2 line event handler to a complex command driven monster for no practical reason (and don't say testebility, this is the main menu, it will be tested every time you run the app).
Now, if you do want to go the pure MVVM route, all you have to do it to make your menu fire a command, first, in some resource section add this style:
<Style x:Key="MenuItemStyle" TargetType="MenuItem">
<Setter Property="Command"
Value="{Binding DataContext.SwitchViewCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"/>
<Setter Property="CommandParameter"
Value="{Binding}"/>
</Style>
This style will make the menu item fire a the SwitchViewCommand on the attached view model with the MenuItem's DataContext as the command parameter.
The actual view is the same as your code with an additional reference to that style as the ItemContainerStyle (so it applies to the menu item and not the content of the DataTemplate):
<DockPanel LastChildFill="False">
<Menu DockPanel.Dock="Top"
ItemsSource="{Binding PageItemsMainMenu}"
ItemTemplate="{StaticResource MainMenuStyle}"
ItemContainerStyle="{StaticResource MenuItemStyle}"/>
<ContentControl
Content="{Binding SelectedPageItem}"/>
</DockPanel>
Now in the view model you need (I used strings because I don't have your PageItem code):
private string _selectedViewItem;
public List<string> PageItemsMainMenu { get; set; }
public string SelectedPageItem
{
get { return _selectedViewItem; }
set { _selectedViewItem = value; OnNotifyPropertyChanged("SelectedPageItem"); }
}
public ICommand SwitchViewCommand { get; set; }
And use whatever command class you use to make the command call this code:
private void DoSwitchViewCommand(object parameter)
{
SelectedPageItem = (string)parameter;
}
Now, when the user clicks a menu item the menu item will call the SwitchViewCommand with the page item as the parameter.
The command will call the DoSwitchViewCommand that will set the SelectedPageItem property
The property will raise the NotifyPropertyChanged that will make the UI update via data binding.
Or, you can write a 2 line event handler, your choice
i could imagine an ObservableCollection in the VM, that holds all the pages to be callable from the menu.
Then bind an ItemsControl And the ContentControl to it to make the ContentControl always show the CurrentItem from that List.
Of course, the menu will only bind to some Title property
whereas the ContentControl will adopt the whole item and plug in some appropriate view according to the type.
Another option is to use a ListBox instead of a menu, style the ListBox to look like a menu and then you can bind to the selected value, like this:
<DockPanel LastChildFill="False">
<ListBox
ItemsSource="{Binding PageItemsMainMenu}"
ItemTemplate="{StaticResource MainMenuStyle}"
IsSynchronizedWithCurrentItem="True"/>
<ContentControl
Content="{Binding PageItemsMainMenu/}"/>
</DockPanel>
Note the IsSynchronizedWithCurrentItem="True" to set the selected item and the {Binding PageItemsMainMenu/} with the trailing slash to use it.

Resources