I have an ItemsControl so that I can display multiple instance of the same template. I need to be able to execute code on event handlers so that I can tell controls apart.
For example: I have a list of groceries, so my DataTemplate contains a "buy" Button for each food. I want to bind said button to code and tell which button was pressed.
How can I accomplish that, considering I'm using MVVM design pattern
** XAML :**
<ItemsControl ItemsSource="{Binding MyItemList}">
<ItemsControl.ItemsTemplate>
<DataTemplate>
<Button Content="Buy" />
</DataTemplate>
</ItemsControl.ItemsTemplate>
</ItemsControl>
So, MyItemList is a List<MyItem> instance. The DataTemplate contains controls that modify values or execute code not present in MyItem:
I have read a lot of articles on biding templates to commands, but I cant find one that uses a list of items.
You need to bind the Button to a Command your ItemsControl's DataContext.
Search for Command in WPF : ( A Common implementation ) :
public class RelayCommand<T> : IRelayCommand
{
private Predicate<T> _canExecute;
private Action<T> _execute;
public RelayCommand(Action<T> execute, Predicate<T> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
private void Execute(T parameter)
{
_execute(parameter);
}
private bool CanExecute(T parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public bool CanExecute(object parameter)
{
return parameter == null ? false : CanExecute((T)parameter);
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
var temp = Volatile.Read(ref CanExecuteChanged);
if (temp != null)
temp(this, new EventArgs());
}
}
In your ViewModel ( The ItemsControl's DataContext , I Hope :) )
private RelayCommand<FoodItem> _addToGroceriesCommand;
public ICommand AddToGroceriesCommand
{
get
{
if (_addToGroceriesCommand == null)
{
_addToGroceriesCommand = new RelayCommand<FoodItem>(OnAddToGroceries);
}
return _addToGroceriesCommand;
}
}
public void OnAddToGroceries(FoodItem newItem)
{
}
XAML :
<ItemsControl ItemsSource="{Binding MyItemList}">
<ItemsControl.ItemsTemplate>
<DataTemplate>
<Button Content="Buy"
Command="{Binding Path=DataContext.AddToGroceriesCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}" />
</DataTemplate>
</ItemsControl.ItemsTemplate>
</ItemsControl>
You should never use events in DataTemplates this will make you use casting and then blow a hole in the whole MVVM pattern. A button has the Command property and you should Bind that property to a command inside your MyItem ViewModel.
If you still need to use an event (for instance you cant bind MouseDown to a command) you shoudl use the EventToCommadn Behaviour which allows you to bind an event to a command. You can read about it here: http://msdn.microsoft.com/en-us/magazine/dn237302.aspx
There are several things you might do.
<Button Content="Add" Click={Click} Tag="{Binding .}" DataContext="{Binding .}" />
DataContext="{Binding .} - sets the whole VM instance to property. You can do the same thing with the Tag property. Sometimes it is usefull to use Tag for these purposes. You can user either of them. Both will work.
public void Click(...)
{
var control = sender as FrameWorkElement;
if(control!= null)
{
var myVM = control.DataContext as MyViewModel;
myVM.DoSomethingWithMyVM();
}
}
You can create a usercontrol that would contain the grid and in the grid you reference the custom usercontrol. That's very flexible. In it's ButtonEventhandler you can access the datacontext and do what you need with it. this is much easier, but you'll have more work with notifications to parrent objects. This is better if you want to reuse this control.
Another thing you can do is to set the datacontext of the button to the whole ViewModel. A last effort solution would be to set the Tag of the button to the whole ViewModel. Better if you are not planing to reuse it.
You can also use this as a resource from the resourceDictionary.
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 been trying to implement this for a while and haven't been able to do it so far, despite having the feeling that this should be something easy.
The difficulty comes from the fact that I have implemented a WPF application using the MVVM pattern. Now, this is my first attempt at both the pattern and the framework, so it is almost guaranteed that I have made mistakes while trying to follow the MVVM guidelines.
My implementation
I have three Views with their respective ViewModels (wired using Prism's AutoWireViewModel method). The MainView has a TabControl with two TabItems, each of witch contains a Frame container with the Source set to one of the other two Views. The following code is an excerpt of the MainView:
<TabControl Grid.Row="1" Grid.Column="1">
<TabItem Header="Test">
<!--TestView-->
<Frame Source="View1.xaml"/>
</TabItem>
<TabItem Header="Results">
<!--ResultsView-->
<Frame Source="View2.xaml"/>
</TabItem>
</TabControl>
My problem
Every time that someone changes to a specific TabItem, I would like to run a method that updates one of the WPF controls included in that View. The method is already implemented and bound to a Button, but ideally, no button should be necessary, I would like to have some kind of Event to make this happen.
I appreciate all the help in advance.
You could for example handle the Loaded event of the Page to either call a method or invoke a command of the view model once the view has been loaded initially:
public partial class View2 : Page
{
public View2()
{
InitializeComponent();
Loaded += View2_Loaded;
}
private void View2_Loaded(object sender, RoutedEventArgs e)
{
var viewModel = DataContext as ViewModel2;
if (viewModel != null)
viewModel.YourCommand.Execute(null);
Loaded -= View2_Loaded;
}
}
The other option would be handle this in the MainViewModel. You bind the SelectedItem property of the TabControl to a property of the MainViewModel and set this property to an instance of either ViewModel2 or ViewModel2, depending on what kind of view you want to display.
You could then call any method or invoked any command you want on these. But this is another story and then you shouldn't hardcode the TabItems in the view and use Frame elements to display Pages. Please take a look here for an example:
Selecting TabItem in TabControl from ViewModel
Okay, so What I have done is Create a Custom Tab Control. I will write out step by step instructions for this, and then you can add edit to it.
Right click on your solution select add new project
Search For Custom Control Library
High Light the name of the class that comes up, and right click rename it to what ever you want I named it MyTabControl.
Add Prism.Wpf to the new project
Add a reference to the new project to where ever your going to need it. I needed to add to just the main application, but if you have a separate project that only has views then you will need to add it to that too.
Inherit your Custom Control From TabControl Like:
public class MyTabControl : TabControl
You will notice that there is a Themes folder in the project you will need to open the Generic.xaml and edit it. it should look like:
TargetType="{x:Type local:MyTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" for some reason this will not let me show the style tags but they will need to be in there as well
Please review this code I got this from Add A Command To Custom Control
public class MyTabControl : TabControl
{
static MyTabControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTabControl), new FrameworkPropertyMetadata(typeof(MyTabControl)));
}
public static readonly DependencyProperty TabChangedCommandProperty = DependencyProperty.Register(
"TabChangedCommand", typeof(ICommand), typeof(MyTabControl),
new PropertyMetadata((ICommand)null,
new PropertyChangedCallback(CommandCallBack)));
private static void CommandCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var myTabControl = (MyTabControl)d;
myTabControl.HookupCommands((ICommand) e.OldValue, (ICommand) e.NewValue);
}
private void HookupCommands(ICommand oldValue, ICommand newValue)
{
if (oldValue != null)
{
RemoveCommand(oldValue, oldValue);
}
AddCommand(oldValue, oldValue);
}
private void AddCommand(ICommand oldValue, ICommand newCommand)
{
EventHandler handler = new EventHandler(CanExecuteChanged);
var canExecuteChangedHandler = handler;
if (newCommand != null)
{
newCommand.CanExecuteChanged += canExecuteChangedHandler;
}
}
private void CanExecuteChanged(object sender, EventArgs e)
{
if (this.TabChangedCommand != null)
{
if (TabChangedCommand.CanExecute(null))
{
this.IsEnabled = true;
}
else
{
this.IsEnabled = false;
}
}
}
private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
{
EventHandler handler = CanExecuteChanged;
oldCommand.CanExecuteChanged -= handler;
}
public ICommand TabChangedCommand
{
get { return (ICommand) GetValue(TabChangedCommandProperty); }
set { SetValue(TabChangedCommandProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (TabChangedCommand != null)
{
TabChangedCommand.Execute(null);
}
}
}
you will need to add the name space in your window or usercontrol like:
xmlns:wpfCustomControlLibrary1="clr-namespace:WpfCustomControlLibrary1;assembly=WpfCustomControlLibrary1"
and here is your control:
<wpfCustomControlLibrary1:MyTabControl TabChangedCommand="{Binding TabChangedCommand}">
<TabItem Header="View A"></TabItem>
<TabItem Header="View B"></TabItem>
</wpfCustomControlLibrary1:MyTabControl>
This is how I'd approach this sort of requirement:
View:
<Window.DataContext>
<local:MainWIndowViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name="tc" ItemsSource="{Binding vms}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type local:uc1vm}">
<local:UserControl1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:uc2vm}">
<local:UserControl2/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabHeading}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Grid>
</Window>
When it has a uc1vm it will be templated into usercontrol1 in the view.
I'm binding to a collection of viewmodels which all implement an interface so I know for sure I can cast to that and call a method.
Main viewmodel for window:
private IDoSomething selectedVM;
public IDoSomething SelectedVM
{
get { return selectedVM; }
set
{
selectedVM = value;
selectedVM.doit();
RaisePropertyChanged();
}
}
public ObservableCollection<IDoSomething> vms { get; set; } = new ObservableCollection<IDoSomething>
{ new uc1vm(),
new uc2vm()
};
public MainWIndowViewModel()
{
}
When a tab is selected, the setter for selected item will be passed the new value. Cast that and call the method.
My interface is very simple, since this is just illustrative:
public interface IDoSomething
{
void doit();
}
An example viewmodel, which is again just illustrative and doesn't do much:
public class uc1vm : IDoSomething
{
public string TabHeading { get; set; } = "Uc1";
public void doit()
{
// Your code goes here
}
}
I appreciate all of your input, but I found an alternative solution. Given the information given by #mm8, I took advantage of the Loaded event but in a way that does not require any code in the code behind.
My solution
In the View which I would like to give this ability to execute a method every time the user selects the TabItem that contains it, I added the following code:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding OnLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
And then simply implemented a DelegateCommand called OnLoadedCommand in the View's respective ViewModel. Inside that command I call my desired method.
Please comment if you spot anything wrong with this approach! I chose to try this since it required the least amount of changes to my code, but I may be missing some vital information regarding problems the solution may cause.
This question already has answers here:
WPF Databinding: How do I access the "parent" data context?
(3 answers)
Closed 5 years ago.
Update
The button I was trying to use, is inside a <DataTemplate>, which apparently caused the issue. Once I tried the code on a button outside the the <ItemsControl> area, it works. Can anyone tell me, why it does not work in a repeated button like <ItemsControl> and <DataTemplate>?
I am trying to implement an MVVM communication pattern, based on an article from TutorialsPoints.com. I have modified the code slightly, but over all it is still very similar to the code in the article. What I want to do is, to write a line in the console once a button is clicked.
With my implementation (see code below) nothing happens when I click the button. I have also tried adding a break point in the OnClick() function to see if that is run, this is not the case. However a break point in the constructor of MyICommand() shows that the class is actually initialized. What am I doing wrong then?
The Button
<Button Content="Do stuff!"
Command="{Binding FakeCommand}"
Cursor="Hand"
Background="Red"
Foreground="White"
BorderThickness="0"
Padding="10 0 10 0" />
The View Model
public class AgreementViewModel : INotifyPropertyChanged
{
public MyICommand FakeCommand { get; set; }
public AgreementViewModel ()
{
LoadAgreements();
FakeCommand = new MyICommand(OnClick, CanClick);
FakeCommand.RaiseCanExecuteChanged();
}
private void OnClick()
{
Console.WriteLine("Something was clicked...");
}
private bool CanClick()
{
return true;
}
}
The Implementation of ICommand
public class MyICommand : ICommand
{
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public event EventHandler CanExecuteChanged = delegate {};
public MyICommand(Action executeMethod)
{
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod)
{
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter)
{
if (_TargetCanExecuteMethod != null)
{
return _TargetCanExecuteMethod();
}
if (_TargetExecuteMethod != null)
{
return true;
}
return false;
}
void ICommand.Execute(object parameter)
{
_TargetExecuteMethod?.Invoke();
}
}
If you have an ItemsControl (as you mention in the updated version) then the DataContext for each instantiation of the DataTemplate will each item of the source collection used in the ItemsSource. To bind to a command in the parent view model you could use ElementName to get to the ItemsControl
<ItemsControl ItemsSource="{Binding Data}" x:Name="root">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="Do stuff!"
Command="{Binding DataContext.FakeCommand, ElementName=root}"
Cursor="Hand"
Background="Red"
Foreground="White"
BorderThickness="0"
Padding="10 0 10 0" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
An alternative if you don't want to use names would be to use RelativeSource to get to the items control:
Command="{Binding DataContext.FakeCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
Note that in both cases the data context will be the ItemsControl, so you need to do DataContext.FakeCommand, DataContext refers here to the data context of ItemsControl
You might also need the item the command was invoked for since it can be invoked for any item in the source collection. To do that you can add a a CommandParameter={Binding}, and the parameter passed the command will be the item (your implementation does not pass the parameter to the delegate, but it could)
In WPF with MVVM it's easy to fire some code when the user changes the tab.
<TabControl Margin="0 5 5 5" Background="#66F9F9F9" SelectedIndex="{Binding TabIndex}">
And then in the ViewModel:
private int _tabIndex;
public int TabIndex
{
get { return _tabIndex; }
set
{
if(_tabIndex != value)
{
_tabIndex = value;
OnPropertyChanged("TabIndex");
if(value == 1)
{
//do something
}
}
}
}
But I'm vaguely uncomfortable with this. What if another developer happens along later and adds another tab in the "1" position. If this is application-critical code (which it is), things will break spectacularly.
Danger can be minimized with unit tests, of course. But it made me wonder: is this seen as bad practice? And is there a way of doing this that allows you to refer to the Tab with a string, rather than an int? I tried noodling with binding to the SelectedValue property, but nothing seemed to happen when the tabs were changed.
You could make a behavior for TabItem, listening for changes to the IsSelected dependency property, and raises a Command when the tab is selected. This can be extended to any number of tabs, each which invokes different commands in the viewmodel. You could also supply a command parameter for any optional context:
class TabSelectedBehavior : Behavior<TabItem>
{
public static readonly DependencyProperty SelectedCommandProperty = DependencyProperty.Register("SelectedCommand", typeof(ICommand), typeof(TabSelectedBehavior));
public ICommand SelectedCommand
{
get { return (ICommand)GetValue(SelectedCommandProperty); }
set { SetValue(SelectedCommandProperty, value); }
}
private EventHandler _selectedHandler;
protected override void OnAttached()
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
if (dpd != null)
{
_selectedHandler = new EventHandler(AssociatedObject_SelectedChanged);
dpd.AddValueChanged(AssociatedObject, _selectedHandler);
}
base.OnAttached();
}
protected override void OnDetaching()
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
if (dpd != null && _selectedHandler != null)
{
dpd.RemoveValueChanged(AssociatedObject, _selectedHandler);
}
base.OnDetaching();
}
void AssociatedObject_SelectedChanged(object sender, EventArgs e)
{
if (AssociatedObject.IsSelected)
{
if (SelectedCommand != null)
{
SelectedCommand.Execute(null);
}
}
}
}
XAML
<TabControl>
<TabItem Header="TabItem1">
<i:Interaction.Behaviors>
<local:TabSelectedBehavior SelectedCommand="{Binding TabSelectedCommand}"/>
</i:Interaction.Behaviors>
</TabItem>
<TabItem Header="TabItem2">
</TabItem>
</TabControl>
In a similar fashion you could also make a behavior for the TabControl, which turns the SelectionChanged event into a command, and pass the Tag object of the selected TabItem as command parameter.
As with all collection controls, the best way to maintain the selected item is to use the SelectedItem property. If you data bind a property of the relevant data type to the TabControl.SelectedItem property, then you'll still be able to tell which tab is selected and select a different one from the view model.
The only problem with this method is that you'll also need to use the TabControl.ItemsSource property to set up the TabItems:
<TabControl ItemsSource="{Binding YourDataItems}" SelectedItem="{Binding YourItem}" />
If you want to try this, then you should know that defining the TabItems can be a little bit confusing. Please refer to the answer from the How to bind items of a TabControl to an observable collection in wpf? question for help with that.
Hy,
I have a menu with a few menu items. I have various other elements like a treeview and some controls. When I open the program all elements in the menu are available. But the first step I have to do is to connect to the server. So all the the other elements shouldn't available till there is made a connection via the connection menu item.
Then I want to show only menu items if a special tree view (for instance the whole item structure) item is choosen for instance all topics. For instance there should be special menu items available if I click a treeview entry in the menu.
Is it possible to accomplish this in xaml?
Update1:
MainWindow.xaml
Title="Service Bus Visualizer" Height="680" Width="1200" Name="Root"
<MenuItem Header="_Read File" Name="readFile" Click="MenuItemReadFile" HorizontalAlignment="Right" Width="187" IsEnabled="{Binding Path=DataContext.IsMonitoring, ElementName=Root}">
<MenuItem.Icon>
<Image Source="Icons/Open.ico" Width="16" Height="16" />
</MenuItem.Icon>
</MenuItem>
MainWindow.xaml.cs
public bool IsMonitoring
{
get
{
return isMonitoring;
}
set
{
isMonitoring = value;
RaisePropertyChanged("IsMonitoring");
}
}
private bool isMonitoring;
public MainWindow()
{
InitializeComponent();
this.IsMonitoring = false;
this.DataContext = this;
Application.Current.MainWindow = this;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
ConnectionWindow.xaml.cs
MainWindow mainWindow = Application.Current.MainWindow as MainWindow;
mainWindow.IsMonitoring = true;
I get no error on the output window but it doesn't work?
Update2:
I have a second parameter which is a ObservableCollection.
MainWindow.xaml
<ListBox Grid.Row="3" Name="Logger" ItemsSource="{Binding Path=DataContext.LoggingList, ElementName=Root}" DisplayMemberPath="Message" IsSynchronizedWithCurrentItem="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Visible" SelectionChanged="BringSelectionIntoView">
</ListBox>
MainWindow.xaml.cs
public static ObservableCollection<Log> LoggingList { get; set; }
public MainWindow()
{
LoggingList = new ThreadSafeObservableCollection<Log>();
this.IsMonitoring = false;
this.DataContext = this;
Application.Current.MainWindow = this;
InitializeComponent();
}
Log.cs
public class Log : INotifyPropertyChanged
{
public string Message {
get
{
return message;
}
set
{
message = value;
NotifyPropertyChanged("Message");
}
}
public string message;
public Log()
{
}
public Log(string message)
{
this.Message = message;
NotifyPropertyChanged("Message");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
Best regards
First, you have two options as far as "availability" is concerned. The "IsEnabled" property, and the "Visible" property. "IsEnabled" is a bool, and determines if the user can click/select/interact with a given element. Generally speaking, if this property is set to false the element will appear "greyed out".
Changing Visibility will make the element appear/disappear entirely. When set to "Visible" (this is actually an enum), it appears normally. When set to "Hidden", the space for it is reserved on the UI, but you can't actually see it. When set to "Collapsed" you cannot see it and no space is reserved for it in the layout.
For your first requirement (waiting to connect to the server), I would use IsEnabled bound to a "IsConnected" property like so:
IsEnabled="{Binding IsConnected}"
That would go on each item that needs to have this behavior.
The "context-specific" menu items are a bit more complicated, but the basic idea would be a binding on Visible for each of the context sensitive items like:
Visible="{Binding Path=SelectedItem, ElementName=MyTreeView, Converter={StaticResource SelectedItemToVisibilityConverter}, ConverterParameter={x:Type ChildItem}"
I am assuming that each items visibility depends on what type of item is selected (child or parent), you should be able to extend the example if I was wrong. The converter would then look like:
public class SelectedItemToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((parameter as Type).IsInstanceOfType(value))
return Visibility.Visible;
else
return Visibility.Collapsed;
}
public object ConvertBack(...)
{
return Binding.DoNothing;
}
}
Please let me know if I can clarify anything. Hopefully that gives you a good starting point for what you are trying to do.
Update:
Looking at your code I see a couple potential problems:
IsMonitoring is declared as a public field. Binding only works with public properties. This property needs to raise the PropertyChanged event for it to work.
In "MainWindow.xaml.cs" you are setting the DataContext multiple times. This isn't how DataContext works in WPF. You need to set it to one object (your ViewModel) that contains all the properties you are interested in binding to. While it is considered bad practice, you could write this.DataContext = this to get it working before you build a ViewModel class.
The IsMonitoring field is declared in your "MainWindow.xaml.cs" file. First, this should be in a view model. Second, the binding is looking for that property on the MenuItem class (likely because it is in some sort of ItemsControl). If you want it on the root data context, give your window some name (like "Root") and use the following binding:
"{Binding Path=DataContext.IsMonitoring, ElementName=Root}"
Hopefully that makes sense. Let me know if I can help further!