I have a menu and some submenus
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "Select a Building",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "Building 4",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "< 500",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel {Header = "Executives" },
new MenuItemViewModel {Header = "Engineers" },
new MenuItemViewModel {Header = "Sales" },
new MenuItemViewModel {Header = "Marketing"},
new MenuItemViewModel {Header = "Support"}
}
},
new MenuItemViewModel { Header = "500 - 999",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel {Header = "Executives" },
new MenuItemViewModel {Header = "Engineers" },
new MenuItemViewModel {Header = "Sales" },
new MenuItemViewModel {Header = "Marketing"},
new MenuItemViewModel {Header = "Support"}
}
}
}
}
}
I am trying to capture the value of each selection the user makes and display them in a listbox. For example a user selects "Building 4" then "500 - 999" then "support" as those values are selected they populated the list box. I have a function in MenuItemViewModel that is called Execute(), this will get the Header of the last value selected, I.e "support" but I cannot figure out how to get that value to the listbox.
Here is the ViewModel
public class MenuItemViewModel
{
private readonly ICommand _command;
string Value;
public ObservableCollection<Cafe> Cafes
{
get;
set;
}
public MenuItemViewModel()
{
_command = new CommandViewModel(Execute);
}
public string Header { get; set; }
public ObservableCollection<MenuItemViewModel> MenuItems { get; set; }
public ICommand Command
{
get
{
return _command;
}
}
public void Execute()
{
MessageBox.Show("Clicked at " + Header);
}
}
And finally the xaml for the MenuItem and ListBox:
<Menu x:Name="buildingMenu" Margin="0,15,0,0" HorizontalAlignment="Left" Height="20" Width="200" ItemsSource="{Binding MenuItems}" >
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</Menu.ItemContainerStyle>
<Menu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=MenuItems}">
<TextBlock Text="{Binding Header}"/>
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
<ListBox Name="selectionListBox" HorizontalAlignment="Right" Height="179" Margin="0,177,55,0" VerticalAlignment="Top" Width="500" />
I have tried to add the Header to a List in the ViewModel but I cannot get anything work. Is there something similar to a combobox's SelectedValue that can be used here? Thanks for the help
This is actually a lot harder to do than you might expect, because you're using menus in a way they're not really designed to be used.
First of all you're going to need a list of items in your view model to bind to, eg:
public ObservableCollection<string> ListItems { get; set; }
And the corresponding binding in your ListBox:
<ListBox ItemsSource="{Binding ListItems}" Name="selectionListBox" />
Next, you'll need to populate this list with items as they're being clicked. This is tricky with the way you've done things at the moment because ListItems will need to be in your main view model, yet your command handler is in MenuItemViewModel. This means you'll either need to add a way for MenuItemViewModel to call it's parent, or better yet move the command handler to the main view model (so that it's now shared) and have the MenuItems pass in their data context object. I use MVVM Lite and in that framework you do something like this:
private ICommand _Command;
public ICommand Command
{
get { return (_Command = (_Command ?? new RelayCommand<MenuItemViewModel>(Execute))); }
}
public void Execute(MenuItemViewModel menuItem)
{
// ... do something here...
}
You don't seem to be using MVVM Lite, but whatever your framework it is it will have support for passing a parameter into your execute function, so I'll leave that for you to look up. Either way your MenuItem Command bindings will now all need to be modified to point to this common handler:
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding ElementName=buildingMenu, Path=DataContext.Command}" />
</Style>
Your other requirement is for the list to populate as each submenu item is selected, and this is where things get nasty. Submenus don't trigger commands when they close, they trigger SubmenuOpened and SubmenuClosed events. Events can't bind to the command handlers in your view model, so you have to use code-behind handlers instead. If you were declaring your MenuItems explicitly in XAML you could just set them directly there, but your Menu is binding to a collection, so your MenuItems are being populated by the framework and you'll have to set the handler by adding an EventSetter to your style:
<Style TargetType="{x:Type MenuItem}">
<EventSetter Event="SubmenuOpened" Handler="OnSubMenuOpened" />
<Setter Property="Command" Value="{Binding ElementName=buildingMenu, Path=DataContext.Command}" />
</Style>
This works, in that calls the specified handler function in your MainWindow code-behind:
private void OnSubmenuOpened(object sender, RoutedEventArgs e)
{
// uh oh, what now?
}
The problem of course is that event handlers exist in the view, but in MVVM we need it in the view model. If you're happy to corrupt your views like this for the sake of getting your application to work then check this SO question for some example code showing how to get it working. However, if you're a hard-core MVVM purist like myself you'll probably want a solution that doesn't involve code-behind, and in that case you'll again need to refer to your framework. MVVM Lite provides a behavior that allows you to convert events to command, you typically use it like this:
<MenuItem>
<i:Interaction.Triggers xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<i:EventTrigger EventName="OnSubmenuOpened">
<cmd:EventToCommand Command="{Binding SubmenuOpenedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
The problem with behaviours though is that you can't set them in a style. Styles are applied to all objects, which is why EventSetter works fine. Behaviours have to be created for each object that they're used for.
So the final piece of the puzzle is that if you have cases in MVVM where you need to set a behaviour in a style then you'll need to use the solution posted by vspivak in his article Using System.Windows.Interactivity Behaviors and Actions in WPF/Silverlight styles. I've used this myself in commercial projects, and it's a nice general solution to the bigger problem of redirecting event handlers to command handlers in styles.
Hope that answers your question. I'm sure it seems ridiculously convoluted, but what you're trying to do is a fairly pathological scenario well outside how WPF is typically used.
Put a Click event on any MenuItem that is created and when the event is fired, acquire the data item off of the source's DataContext which seeded the MenuItem.
Example
I created a recent list of operation which were needed as submenu's under a Recent menu. I seeded the sub list as a model of typeMRU which was used to load the menu *via ItemsSource and held in an ObservableCollection<MRU>() MRUS :
Xaml
<MenuItem Header="Recent"
Click="SelectMRU"
ItemsSource="{Binding Path=MRUS}">
...
</MenuItem>
Codebehind
private void SelectMRU(object sender, RoutedEventArgs e)
{
try
{
var mru = (e.OriginalSource as MenuItem).DataContext as MRU;
if (mru is not null)
{
VM.ClearAll();
this.Title = mru.Name;
ShowJSON(File.ReadAllText(mru.Address));
}
}
catch (Exception ex)
{
VM.Error = ex.Demystify().ToString();
}
}
public class MRU
{
public string Name { get; set; }
public string Address { get; set; }
public string Data { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(Name);
public bool IsFile => !string.IsNullOrWhiteSpace(Address);
public bool IsData => !IsFile;
public MRU(string address)
{
if (string.IsNullOrWhiteSpace(address)
|| !File.Exists(address)) return;
Address = address;
Name = System.IO.Path.GetFileName(address);
}
// This will be displayed as a selectable menu item.
public override string ToString() => Name;
}
Note that the full Menu code can be viewed in public gist Gradient Menu Example with MRU
Related
What I'm trying to accomplish
I'm trying to have my nested menu item change the shown user control. In more technical terms, I'm trying to:
Get a click event attached to a nested MenuItem (from my MyMenu.cs file - implements INotifyPropertyChanged), to...
Use RoutedEventHandler (maybe from the MyMenu.cs file? - implements UserControl), to...
Call the SwitchScreen method (from my MainWindow.cs file - implements Window)
Where I'm getting stuck
I can't seem to find a way to add the click event to the appropriate menu item.
My current logic also requires the original sender to be passed as an argument so that I can identify the correct MySubview to display.
XAML Handler
I've tried adding the click event in xaml as follows, but it only adds the handler to the first menu item level (not to nested menu item elements).
<MenuItem ItemsSource="{Binding Reports, Mode=OneWay}" Header="Reports">
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<EventSetter Event="Click" Handler="MenuItem_Click"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
C# Setter
I've tried adding a setter, suggested in this answer, but I can't seem to create a click event from MyMenu.cs to MyMenuUserControl.cs.
Style style = new Style();
style.BasedOn = menuItem2.Style;
style.Setters.Add(new EventSetter( /* ??? */ ));
C# ICommand
I've tried using ICommand, suggested in this answer, but I can't seem to create a relay command from MyMenu.cs to MyMenuUserControl.cs.
I may be doing something wrong in one of these attempts, but I'm now past the point of playing around and ready to throw in the towel.
Notes
Actual structure
In reality, my actual code has n-nested foreach loops to generate the menu and I remove a level of nesting if a the foreach enumerable (e.g. myObjects) only has one element.
The removal of a level of nesting also moves the click event up one level.
My final menu could look something like this:
My menu items:
Item (menuItem1)
Item (menuItem2)
Item (menuItem3) + click event
Item (menuItem3) + click event
Item (menuItem2) + click event (see A)
Item (menuItem1) + click event (see B)
A: Only one menuItem3 is nested, so we remove it (it's redundant) and we move the click event up to menuItem2.
B: Only one menuItem2 is nested, and it only has one menuItem3. Both are removed as they're redundant and we move the click event is moved to menuItem1.
This is why I'd like to maintain the creation of the menu items in the MyMenu class.
Other suggestions
I could be going about this completely wrong and I'm open to suggestions that change the way I'm going about this.
Code
MyMenu.cs
The constructor in this class generates my menu items and its sub-menu items.
This is where I'm trying to add a click event.
class MyMenu : INotifyPropertyChanged
{
private List<MenuItem> menuItems = new List<MenuItem>();
public List<MenuItem> MenuItems
{
get { return menuItem; }
set
{
menuItem = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public List<Tuple<MyObject, MenuItem>> Map { get; private set; } = new List<Tuple<MyObject, MenuItem>>();
public MyMenu(List<MyObject> myObjects)
{
foreach(MyObject myObject in myObjects)
{
MenuItem menuItem1 = new MenuItem { Header = myObject.Name };
foreach(string s in myObject.Items)
{
MenuItem menuItem2 = new MenuItem { Header = s };
// Add click event to menuItem2 here
menuItem1.Items.Add(menuItem2);
Map.Add(new Tuple<MyObject, MenuItem>(myObject, menuItem2));
}
MenuItem.Add(menuItem1);
}
}
private void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
MyMenuUserControl.xaml
Minimal code sample UserControl (uses default xmlns attributes).
MyMenuUserControl.xaml.cs only has constructor with InitializeComponent();
<UserControl>
<!-- xmlns default attributes in UserControl above removed for minimal code -->
<Menu>
<Menu.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel VerticalAlignment="Stretch"/>
</ItemsPanelTemplate>
</Menu.ItemsPanel>
<MenuItem ItemsSource="{Binding MenuItems, Mode=OneWay}" Header="My menu items"/>
</Menu>
</UserControl>
MyDataContext.cs
Minimal code sample (same PropertyChangedEventHandler and OnPropertyChanged() code as MyMenu.cs).
Constructor simply sets Menu and Subviews properties.
class MyDataContext : INotifyPropertyChanged
{
private MyMenu menu;
public MyMenu Menu
{
get { return menu; }
set
{
menu = value;
OnPropertyChanged();
}
}
private List<MySubview> mySubviews;
public List<MySubview> MySubviews
{
get { return mySubviews; }
set
{
mySubviews = value;
OnPropertyChanged();
}
}
// ... rest of code removed to maintain minimal code
}
MainWindow.xaml.cs
Subview contains a property of MyObject type.
This allows me to use MyMenu's Map property to identify which Subview to display for a given MenuItem's click.
Yes, making the map at the MainWindow map might be easier, however the logic I have in MyMenu is a minimal example (see Notes for more info).
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// I get my data here
List<MyObject> myObjects = ...
List<MySubview> mySubviews = ...
DataContext = new MyDataContext(new MyMenu(myObjects), new MySubviews(mySubviews));
}
private void SwitchScreen(object sender, RoutedEventArgs e)
{
MyDataContext c = (MyDataContext)DataContext;
MyObject myObject = c.MyMenu.Map.Where(x => x.Item2.Equals(sender as MenuItem)).Select(x => x.Item1).First();
MySubview shownSubview = c.MySubviews.Where(x => x.MyObject.Equals(myObject)).First();
c.MySubviews.ForEach(x => x.Visibility = Visibility.Collapsed);
shownSubview.Visibility = Visibility.Visible;
}
}
Wpf is designed to be used via the MVVM pattern. You appear to be trying to manipulate the visual tree directly, which is probably where a lot of your problems are coming from since you appear to be half way between worlds.
What is MyMenu.cs? It looks like a view model but it contains visual items (MenuItem). VMs should not contain any visual classes. They are a data abstraction of the view.
It looks like your MyMenuVM.cs should just expose your List <MyObject>, and your view menu should bind to that. MenuItem already has a built in ICommand (after all menus are made for clicking), so you don't need to add your own click handlers. Instead you bind MenuItem.Command to a command in your VM, and possibly bind CommandParameter to supply which MyObject is firing the command.
In short, I would read up a bit about MVVM because it will make your code far cleaner and easier to understand and hopefully prevent these kind of issues.
Menu can build its items from ItemsSource using any IEnumerable object. One thing you should do - set a DataTemplate for mapping MenuItem's properties to your VM properties.
I collected some links for you that may be useful in understanding how it may be done with MVVM:
RelayCommand class - refer to Relaying Command Logic section. From my (WPF newbie's) perspective it's the best way of using Commands.
HierarchicalDataTemplate - same as DataTemplate but with ItemsSource.
Trick with Separators - may help with making Menu containing not only MenuItems in its ItemsSource (tested!)
ObservableCollection - use it instead of List for UI purpose. It fires CollectionChanged event inside when you dinamically add or remove items. And Control with ItemsSource updates its layout immediately, out-of-the-box.
Why not just a collection of Controls?
Because you may break you application causing an Exception while trying to interact with UI Elements from different Thread. Yes, you may use Dispatcher.Invoke for fix but there's a better way avoiding it: simply use Binding. Thus, you may forget about Dispatcher.Invoke-everywhere problem.
Simple Example
Using single RelayCommand for all MenuItem instances.
MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
Title="MainWindow" Height="300" Width="400">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<local:MenuItemContainerTemplateSelector x:Key="MenuItemContainerTemplateSelector"/>
<Style x:Key="SeparatorStyle" TargetType="{x:Type Separator}" BasedOn="{StaticResource ResourceKey={x:Static MenuItem.SeparatorStyleKey}}"/>
<Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Command" Value="{Binding DataContext.MenuCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Setter Property="CommandParameter" Value="{Binding CommandName}"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Menu Grid.Row="0" >
<MenuItem Header="Menu" ItemsSource="{Binding MenuItems}" UsesItemContainerTemplate="True" ItemContainerTemplateSelector="{StaticResource MenuItemContainerTemplateSelector}">
<MenuItem.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:MyMenuItem}" ItemsSource="{Binding Items}" >
<MenuItem Style="{StaticResource MenuItemStyle}" UsesItemContainerTemplate="True" ItemContainerTemplateSelector="{StaticResource MenuItemContainerTemplateSelector}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:MySeparator}">
<Separator Style="{StaticResource SeparatorStyle}"/>
</DataTemplate>
</MenuItem.Resources>
</MenuItem>
</Menu>
</Grid>
</Window>
RelayCommand.cs
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
MenuItemContainerTemplateSelector.cs
public class MenuItemContainerTemplateSelector : ItemContainerTemplateSelector
{
public override DataTemplate SelectTemplate(object item, ItemsControl parentItemsControl) =>
(DataTemplate)parentItemsControl.FindResource(new DataTemplateKey(item.GetType()));
}
MenuItemViewModel.cs
public interface IMyMenuItem
{
}
public class MySeparator : IMyMenuItem
{
}
public class MyMenuItem : IMyMenuItem, INotifyPropertyChanged
{
private string _commandName;
private string _header;
private ObservableCollection<IMyMenuItem> _items;
public string Header
{
get => _header;
set
{
_header = value;
OnPropertyChanged();
}
}
public string CommandName
{
get => _commandName;
set
{
_commandName = value;
OnPropertyChanged();
}
}
public ObservableCollection<IMyMenuItem> Items
{
get => _items ?? (_items = new ObservableCollection<IMyMenuItem>());
set
{
_items = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
private ObservableCollection<IMyMenuItem> _menuItems;
private ICommand _menuCommand;
public ObservableCollection<IMyMenuItem> MenuItems
{
get => _menuItems ?? (_menuItems = new ObservableCollection<IMyMenuItem>());
set
{
_menuItems = value;
OnPropertyChanged();
}
}
public ICommand MenuCommand => _menuCommand ?? (_menuCommand = new RelayCommand(param =>
{
if (param is string commandName)
{
switch (commandName)
{
case "Exit":
Application.Current.MainWindow.Close();
break;
default:
MessageBox.Show("Command name: " + commandName, "Command executed!");
break;
}
}
}, param =>
{
return true; // try return here false and check what will happen
}));
public MainViewModel()
{
MenuItems.Add(new MyMenuItem() { Header = "MenuItem1", CommandName = "Command1" });
MenuItems.Add(new MyMenuItem() { Header = "MenuItem2", CommandName = "Command2" });
MyMenuItem m = new MyMenuItem() { Header = "MenuItem3" };
MenuItems.Add(m);
m.Items.Add(new MyMenuItem() { Header = "SubMenuItem1", CommandName = "SubCommand1" });
m.Items.Add(new MySeparator());
m.Items.Add(new MyMenuItem() { Header = "SubMenuItem2", CommandName = "SubCommand2" });
m.Items.Add(new MyMenuItem() { Header = "SubMenuItem3", CommandName = "SubCommand3" });
MenuItems.Add(new MySeparator());
MenuItems.Add(new MyMenuItem() { Header = "Exit", CommandName = "Exit" });
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I have a TabControl with multiple DataTemplate. the first DataTemplate will be used for search reasons and the second will be for displaying items obtained from that search. My XAML code will be as follows:
<UserControl.Resources>
<!--First template-->
<DataTemplate>
<!-- I will have a DataGrid here-->
</DataTemplate>
<!--Second template-->
<DataTemplate >
<!-- I will have details of one item of the DataGrid-->
</DataTemplate>
</UserControl.Resources>
<TabControl ItemsSource="{Binding }"/>
What I want to accomplish is that in the TabControl the first tab will contain the first DataTemplate (the search template) and when I double click on one row of my DataGrid, a tab will be added with the details of that row (in other words a tab with the second template).
Since I am using MVVM, I thought of creating two UserControls, one for each template and then catch the double click event, but after this I don't know how to add a tab since now my search template is a UserControl seperated from the one that contains the TabControl.
So how do I do this?
UPDATE:
As I read the answers I think I wasn't very clear in stating the problem.
My problem is how to add tabs with the second template, by catching double click events from the first template. I don't have any problem in adding the two templates independently.
Rather can creating two UserControls, you can create and use a DataTemplateSelector in order to switch different DataTemplates in.
Basically, create a new class that inhereits from DataTemplateSelector and override the SelecteTemplate method. Then declare an instance of it in the XAML (much like a value converter), and then apply it to ContentTemplateSelector property of the TabControl.
More info can be found here.
If you're going to do this with MVVM, your tab control should be bound to some ObservableCollection in your VM, and you just add and remove VM's to the collection as needed.
The VMs can be any type you like and your DataTemplates will show the correct view in the tab just like any other view, so yes, create two UserControls for the two views.
public class MainVM
{
public ObservableCollection<object> Views { get; private set; }
public MainVM()
{
this.Views = new ObservableCollection<object>();
this.Views.Add(new SearchVM(GotResults));
}
private void GotResults(Results results)
{
this.Views.Add(new ResultVM(results));
}
}
There are two options: Use datatemplate selector, or use implicit datatemplates and different types for each tabitem.
1. DataTemplateSelector:
public ObservableCollection<TabItemVM> Tabs { get; private set; }
public MainVM()
{
Tabs = ObservableCollection<TabItemVM>
{
new TabItemVM { Name="Tab 1" },
};
}
void AddTab(){
var newTab = new TabItemVM { Name="Tab 2" };
Tabs.Add(newTab);
//SelectedTab = newTab; //you may bind TabControl.SelectedItemProperty to viewmodel in order to be able to activate the tab from viewmodel
}
public class TabItemTemplateSelector : DataTemplateSelector
{
public DataTemplate Tab1Template { get; set; }
public DataTemplate Tab2Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var tabItem = item as TabItemVM;
if (tabItem.Name == "Tab 1") return Tab1Template;
if (tabItem.Name == "Tab 2") return Tab2Template;
return base.SelectTemplate(item, container);
}
}
<local:TabItemTemplateSelector
x:Key="TabItemTemplateSelector"
Tab1Template="{StaticResource Tab1Template}"
Tab2Template="{StaticResource Tab2Template}" />
2. Implicit Data Templates:
public class MainVM : ViewModelBase
{
public ObservableCollection<TabItemVM> Tabs { get; private set; }
public MainVM()
{
Tabs = new ObservableCollection<TabItemVM>
{
new Tab1VM(),
};
}
void AddTab()
{
var newTab = new Tab2VM()
Tabs.Add(newTab);
//SelectedTab = newTab;
}
}
public class TabItemBase
{
public string Name { get; protected set; }
}
public class Tab1VM : TabItemBase
{
public Tab1VM()
{
Name = "Tab 1";
}
}
public class Tab2VM : TabItemBase
{
public Tab2VM()
{
Name = "Tab 2";
}
}
<UserControl.Resources>
<!--First template-->
<DataTemplate DataType="local:Tab1VM">
<!-- I will have a DataGrid here-->
</DataTemplate>
<!--Second template-->
<DataTemplate DataType="local:Tab2VM">
<!-- I will have details of one item of the DataGrid-->
</DataTemplate>
</UserControl.Resources>
Variations of this question seems to be quite common, but I have yet to find a solution that works for me. I am attempting to place a Dropdown menu within a Button.ContextMenu, using an Observable collection, and thought I was on the right track, with one piece missing: I have yet to be able to get the index of the item selected, and although I can see my collection in the debugger, I am beginning to wonder if the items are really getting found, will explain as I go. First, the XAML...you can see that I have a binding for the Button Content, and the idea is that after a menu item gets selected, my code behind will update that property. Which it could, if I could get the index of the collection that is being collected:
<Button x:Name="DeviceSelMenuButton" Content="{Binding DeviceID_and_SN, Mode=TwoWay}" HorizontalAlignment="Left" Height="28" Margin="25,103,0,0" VerticalAlignment="Top" Width="187" FontSize="14" Click="DeviceSelMenuButton_Click">
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding DeviceID_SN_Collection, Mode=TwoWay}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="IsCheckable" Value="true"/>
<Setter Property="Command" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=DataContext.MyCommand}"/>
<Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ContextMenu}}}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
As you can imagine, I have tried many variations for the CommandParameter bindings, but with this one I can at least see some things in my ICommand method. The thing that troubles me is that at the first push of the button (and no there are no errors in the Output window regarding the bindings), under the ContextMenu cm's Items property, I see a legitimate items label under items.CurrentItem, and item.CurrentPosition is 0 -- which at first looked promising, was hoping I could use that as the index, until I realized I was looking at the last item and so it must be meaningless. After that, the second time, and all subsequent pushes of the button, the items.CurrentItem is null, and the items.Current position is 0xffffffff. Pasting in the relevant pieces of code, starting with the class defining the collection, ICommand, etc:
class CustomDeviceGUI : INotifyPropertyChanged
{
// Declare the event
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _deviceDisplayString;
private ICommand UpdateMenuICommand;
List<string> ControllerDeviceList = new List<string>();
private System.Collections.ObjectModel.ObservableCollection<string> _DeviceID_SN_Collection = new System.Collections.ObjectModel.ObservableCollection<string>();
// CTOR
public CustomDeviceGUI()
{
ControllerDeviceList.Add("CustomDevice Device 1");
ControllerDeviceList.Add("CustomDevice Device 2");
ControllerDeviceList.Add("CustomDevice Device 3");
ControllerDeviceList.Add("CustomDevice Device 6");
UpdateDeviceID(3); // TODO Get from GUI!!!
}
#region CustomDeviceGUI Properties
public System.Collections.ObjectModel.ObservableCollection<string> DeviceID_SN_Collection
{
get
{
_DeviceID_SN_Collection.Clear();
foreach (string str in ControllerDeviceList)
{
_DeviceID_SN_Collection.Add(str);
}
return _DeviceID_SN_Collection;
}
private set
{
_DeviceID_SN_Collection = value;
}
}
public string DeviceID_and_SN
{
get
{
return _deviceDisplayString;
}
private set
{
_deviceDisplayString = value;
}
}
public ICommand MyCommand
{
get
{
if (UpdateMenuICommand == null)
UpdateMenuICommand = new MyGuiCommand();
return UpdateMenuICommand;
}
set
{
UpdateMenuICommand = value;
RaisePropertyChangeEvent("MyCommand"); // ????
}
}
public void UpdateDeviceID(int deviceID)
{
this._deviceDisplayString = ControllerDeviceList[deviceID];
RaisePropertyChangeEvent("DeviceID_and_SN");
RaisePropertyChangeEvent("DeviceID_SN_Collection");
}
public class MyGuiCommand : ICommand
{
// Two events are kicked off when the command is executed
public static event UpdateDeviceSelectedEventHandler UpdateDeviceSelectedEvent;
// defining signature for any event handlers for the events we create here
public delegate void UpdateDeviceSelectedEventHandler(int deviceIndex);
public void Execute(object parameter)
{
ContextMenu cm = (ContextMenu)parameter;
var itemSource = cm.ItemsSource;
var itemBG = cm.ItemBindingGroup;
var items = cm.Items;
UpdateDeviceSelectedEvent(1); // TODO parameter with index from GUI
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged // was ;
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
} // class CustomDeviceGUI
and finally the relevant code from MainWindow.xaml.cs, not a lot happening here:
// register for the event from the ICommand.Execute
WpfBindingAttempts.CustomDeviceGUI.MyGuiCommand.UpdateDeviceSelectedEvent += new WpfBindingAttempts.CustomDeviceGUI.MyGuiCommand.UpdateDeviceSelectedEventHandler(UpdateDeviceSelectedAfterSwitch);
// Handles event that occurs when a different device is selected
// via the dropdown menu -- sets the active device, and updates its ID/SN
void UpdateDeviceSelectedAfterSwitch(int deviceIndex)
{
_customDeviceGui.UpdateDeviceID(deviceIndex);
}
code behind for button:
private void DeviceSelMenuButton_Click(object sender, RoutedEventArgs e)
{
// " (sender as Button)" is PlacementTarget
(sender as Button).ContextMenu.IsEnabled = true;
(sender as Button).ContextMenu.PlacementTarget = (sender as Button);
(sender as Button).ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
(sender as Button).ContextMenu.IsOpen = true;
}
Think that's everything. Any help is greatly appreciated. If this looks familiar, it is. I got over the first hurdles and slammed into this brick wall.
I don't think there's any direct way to return the selected index of an item within a ContextMenu. I don't believe the CurrentItem property holds anything to do with which item has been selected. I'm not sure exactly what this property does do (it might be something used internally by the framework), but I would recommend that you ignore it.
Instead of making the context menu be the CommandParameter, make the items in your collection the CommandParameters. To do this, change the CommandParameter setter to the following:
<Setter Property="CommandParameter" Value="{Binding}" />
Then, pass the list of all devices to your MyGuiCommand, in a constructor argument for example. Finally, in your Execute method, search within this list of devices to find the selected device, which will be in the parameter.
All, I have a custom DataGridView control which overrides the DataGidView's OnItemsSourceChanged event. Inside this event I need to get a reference to a data set in the relevant ViewModel. Code is
public class ResourceDataGrid : DataGrid
{
protected override void OnItemsSourceChanged(
System.Collections.IEnumerable oldValue,
System.Collections.IEnumerable newValue)
{
if (Equals(newValue, oldValue))
return;
base.OnItemsSourceChanged(oldValue, newValue);
ResourceCore.ResourceManager manager = ResourceCore.ResourceManager.Instance();
ResourceDataViewModel resourceDataViewModel = ?? // How do I get my ResourceDataViewModel
List<string> l = manger.GetDataFor(resourceDataViewModel);
...
}
}
On the marked line I want to know how to get a reference to ResourceDataViewModel resourceDataViewModel. The reson is that i have multiple tabs each tab contains a data grid and ascociated ViewModel, the ViewModel holds some data that I need to retrieve [via the ResourceManager] (or is there another, better way?).
The question is, from the above event, how can I get the ascociated ResourceDataViewModel?
Thanks for your time.
Get the DataContext and cast it to the view-model type:
var viewModel = this.DataContext as ResourceDataViewModel
Put a static reference to it on your app, when the VM is created place its reference on the static and access it as needed.
You ask if there is a better way... In my experience if you find yourself subclassing a UI element in WPF there ususally is.
You can get away from embedding business logic (the choice of which data to display in the grid), by databinding your entire tab control to a view model.
To demonstrate - here is a very simple example. This is my XAML for the window hosting the tab control:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabName}"></Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<DataGrid ItemsSource="{Binding TabData}"></DataGrid>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The data context of my window is a TabsViewModel (I am using the NotificationObject that can be found in the PRISM NuGet Package):
public class TabsViewModel: NotificationObject
{
public TabsViewModel()
{
Tabs = new[]
{
new TabViewModel("TAB1", "Data 1 Tab 1", "Data 2 Tab1"),
new TabViewModel("TAB2", "Data 1 Tab 2", "Data 2 Tab2"),
};
}
private TabViewModel _selectedTab;
public TabViewModel SelectedTab
{
get { return _selectedTab; }
set
{
if (Equals(value, _selectedTab)) return;
_selectedTab = value;
RaisePropertyChanged(() => SelectedTab);
}
}
public IEnumerable<TabViewModel> Tabs { get; set; }
}
public class TabViewModel
{
public TabViewModel(string tabName, params string[] data)
{
TabName = tabName;
TabData = data.Select(d => new RowData(){Property1 = d}).ToArray();
}
public string TabName { get; set; }
public RowData[] TabData { get; set; }
}
public class RowData
{
public string Property1 { get; set; }
}
This is obviously an over simplified case, but it means that if there is any business logic about precisely what data to show in each tab, this can reside in one of the view models, as opposed to the code behind. This gives you all the 'separation of concerns' benefits that MVVM is designed to encourage...
I have made a tree View in wpf Using MVVM .
it is working fine but here is one problem that leaf node contains some checkboxes and user have only two options either to select one or none .
So here how i can restricted user to select maximum only one cold drink.
I did one trick but it didn't work that when i have already selected a drink and then i select another one than i set the last selected value in the observable collection to false but it doesn't affect on view and selected check boxes remains selected although in collection only one option's value is true.
I cant use radio button instedof checkbox becasue user can select none of the options and i cant give an additional option for none of the above.
If any one have any solution so please let me know I'll be very thankful.
updated question:
i think i didn't define my problem in a proper way so i am giving my code snipperts here hope by this i'll get the solution o f my problem...
My View Model Class
namespace TestViewModels
{
public class ViewModel :ViewModelBase
{
private ObservableCollection<AvailableProducts> _MyTreeViewProperty
public ObservableCollection<AvailableProducts> MyTreeViewProperty
{
get { return _MyTreeViewProperty
set { _MyTreeViewProperty value;
RaisePropertyChanged("MyTreeViewProperty");}
}
}
public class AvailableProducts
{
private string _BrandName;
public string BrandName
{
get { return _BrandName
set { _BrandName = value; }
}
private bool _IsExpanded;
public bool IsExpanded
{
get
{
return _IsExpanded;
}
set
{
_IsExpanded = value;
}
}
private ObservableCollection<ProductTypes> _MyProductTypes
public ObservableCollection<ProductTypes> MyProductTypes
{
get { return _MyProductTypes}
set { _MyProductTypes= value; }
}
}
public class ProductTypes
{
private string _ProductTypeName;
public string ProductTypeName
{
get { return _ProductTypeName;
set { _ProductTypeNamevalue; }
}
private ObservableCollection<ProductSubTypes> _ProdSubTypes;
public ObservableCollection<ProductSubTypes> ProdSubTypes
{
get { return _ProdSubTypes;}
set { _ProdSubTypes;= value; }
}
}
public class ProductSubTypes
{
private string _ProductSubTypeName;
public string ProductSubTypeName
{
get { return _ProductSubTypeName;
set { _ProductSubTypeName;}
}
private int _ParentID;
public int ParentID
{
get { return _ParentID;}
set { _ParentID;= value; }
}
private bool _IsAssigned;
public bool IsAssigned
{
get { return _IsAssigned; }
set
{
_IsAssigned = value;
if _ParentID;!= 0)
{
//updating data in database
//Calling and setting new collection value in property
//issue : updated collection sets in setter of MyTreeViewProperty but before calling getter
// it comes to IsAssigned getter so view doesnt get updated collection of MyTreeViewProperty
}
RaisePropertyChanged("IsAssigned");
}
}
}
}
View
<Page x:Class="ShiftManagerViews.Pages.ProductTreeSelection
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding ProductsTree, Source={StaticResource Locator}}"
mc:Ignorable="d" Width="870" Height="665"
>
<TreeView Margin="10,10,0,13" ItemsSource="{Binding MyTreeViewProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="800" Height="Auto" MinHeight="400" MaxHeight="800">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:AvailableProducts}"
ItemsSource="{Binding MyProductTypes}">
<WrapPanel>
<Image Width="20" Height="20" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding BrandName}" FontSize="14"/>
</WrapPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:ProductTypes}"
ItemsSource="{Binding ProdSubTypes}">
<WrapPanel>
<Image Width="18" Height="15" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding ProductTypeName}" FontSize="13"/>
</WrapPanel>
</HierarchicalDataTemplate>
<!-- the template for showing the Leaf node's properties-->
<DataTemplate DataType="{x:Type local:ProductSubTypes}">
<StackPanel>
<CheckBox IsChecked="{Binding IsAssigned, Mode=TwoWay}" Content="{Binding ProductSubTypeName}" Height="25">
</CheckBox>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
What about using a ListBox to display sub-items instead of a TreeView? You can style that so the items contain a CheckBox to show IsSelected instead of highlighting the item.
I'd suggest your user interface is wrong. If the user can only pick one then it would be better to swap these for radio buttons and add a "None of the above" option. That'll then give you the behaviour you want for free and your UI will be more intuitive.
EDIT: Since you say you can't add a "None" option and want to use a checkbox (even though I strongly disagree on checkboxes where a radio button is more appropriate - a common UI error)...
The technical problem you are probably facing is that an ObservableCollection only raises notification events if the collection itself changes. i.e. Only if items are added or removed. It does not raised events when items within the collection change, therefore the changing the status of the checkbox in the code will not raise the event for the UI binding to act on.
One solution to this to write a custom class that extends ObservableCollection that does provide this behaviour
From MSDN:
If you need to know if someone has changed a property of one of the
items within the collection, you'll need to ensure that the items in
the collection implement the INotifyPropertyChanged interface, and
you'll need to manually attach property changed event handlers for
those objects. No matter how you change properties of objects within
the collection, the collection's PropertyChanged event will not fire.
As a matter of fact, the ObservableCollection's PropertyChanged event
handler is protected—you can't even react to it unless you inherit
from the class and expose it yourself. You could, of course, handle
the PropertyChanged event for each item within the collection from
your inherited collection
I upvoted Rachel's answer, it is a common way in WPF to databind sets of radio buttons or check boxes. If you still want to go the tree view way, below code works. All view related code is in the view, so below code follows MVVM principles. If you are a MVVM purist you can put the code behind and a TreeView control in a user control if you do not want any code behind.
XAML:
<TreeView ItemsSource="{Binding Path=Drinks}">
<TreeView.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding .}" Checked="OnCheckBoxChecked" Unchecked="OnCheckBoxUnchecked" Loaded="OnCheckBoxLoaded" />
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Code behind + VM:
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
private void OnCheckBoxChecked(object sender, System.Windows.RoutedEventArgs e)
{
foreach (CheckBox checkBox in _checkBoxes.Where(cb => cb != sender))
{
checkBox.IsChecked = false;
}
(DataContext as VM).CurrentDrink = (sender as CheckBox).Content.ToString();
}
private void OnCheckBoxUnchecked(object sender, System.Windows.RoutedEventArgs e)
{
(DataContext as VM).CurrentDrink = null;
}
private void OnCheckBoxLoaded(object sender, System.Windows.RoutedEventArgs e)
{
_checkBoxes.Add(sender as CheckBox);
}
private List<CheckBox> _checkBoxes = new List<CheckBox>();
}
public class VM
{
public List<string> Drinks
{
get
{
return new List<string>() { "Coffee", "Tea", "Juice" };
}
}
public string CurrentDrink { get; set; }
}
I did one trick but it didn't work that when i have already selected a
drink and then i select another one than i set the last selected value
in the observable collection to false but it doesn't affect on view
and selected check boxes remains selected although in collection only
one option's value is true.
Make sure that your child objects (AvailableProducts
and SubProductTypes) also implement INotifyPropertyChanged, this will make sure that the UI receives changes when modify the object.
Once all of you objects update the UI properly you will be able to layer in, and test, whatever custom business logic you need.
So if you have a product type that can only have one sub chosen, you could add a property on ProductType called OnlyAllowOneChild. Whenever, a child object raises a IsAssigned changed event, the parent can set false all other children. This of course requires you to have the parent either register for the children's PropertyChangedEvent, or got grab an EventAggregator (MVVMLight Messenger, or PRISM EvenAggregator) and create a messaging system.
Finally i am succeeded to solve my problem.
on Is Assigned property i am updating my database values and calling a method in view using MVVM Light messaging and passing currently selected leaf's parent id in it as a parameter...
Added a property in class Product Types to expand the parent node of the last selected leaf..
In view's method i am refreshing data context's source and passing currently selected leaf's parent id tO the VM to set its Is Expanded property value to true...
By this my view is working perfectly as same as i want...
If any body have solution better than this than I'll be happy to know.