WPF TreeView context menu is disabled if no items exist - c#

I am trying to show a ContextMenu in a TreeView. Some entries must be available whether an item was selected or not, but all commands are disabled until I populate the TreeView with at least one item:
<TreeView Name="myTreeView" Width="200px">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Command="New" IsEnabled="True" />
</ContextMenu>
</TreeView.ContextMenu>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Path=Title}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
However, the menu item is still disabled:
The very same command is enabled in the File menu in the menu bar and there is no CanExecute attribute.
How can I enable the context menu entry even if no item exists?

The issue is that the DataContext of the ContextMenu (i.e. where it's looking to bind the New command) is the tree-view node, not the tree view itself. Great if you've got commands related to the node - editing, moving, changing settings.
Not so good for the few that are pan-node like adding and deleting.
As it's looking in the node's DataContext (and no nodes exit) it can't find the command (and it doesn't make sense for it to be there anyway, as the object that manages the TreeView should be creating new items, not the items themselves).
The solution is to bind to a New command that's not in the DataContext of the item, but the TreeView. There's the frustration of dealing with data-binding with ContextMenu... as it's not in the same visual tree as the rest of the window it's often frustrating to deal with.
A solution is to reference the PlacementTarget of the context menu like this:
<TreeView Name="myTreeView" Width="200px">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit (This command exists in the Node's ViewModel)" Command="{Binding Edit}"/>
<MenuItem Header="New (This command exists in the Window's ViewModel)" Command="{Binding PlacementTarget.DataContext.New, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>
</TreeView.ContextMenu>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Path=Title}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Further questions
An example of adding a command as a static resource (Change Window to UserControl if you're in a view that's a UserControl):
<Window.Resources>
<local:MyCommand x:Key="MyCommand"/>
</Window.Resources>
Then referenced with:
<MenuItem Header="MyCommand" Command="{StaticResource MyCommand}"/>
Binding to your commands in the ViewModel (i.e. DataContext) is done like in the first example. In the same way you bind the Title, you can bind to any property, such as an ICommand.
So for a view:
<MenuItem Header="New" Command="{Binding New}"/>
The View Model has a property NewCommand named New:
public NewCommand New { get; private set; }
People often use this because they have a generic ICommand that takes a delegate so they can configure all the actions that relate to that ViewModel. For example:
public class MyCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public Action<object> Action { get; set; }
public MyCommand(Action<object> action)
{
Action = Action;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
Action(parameter);
}
}
Then in the ViewModel, instead of having loads of ICommand classes all implemented, we can just re-use this and get it to do different things:
public MyCommand New { get; private set; }
public MyCommand Delete { get; private set; }
public MyCommand ClearAll { get; private set; }
public MyViewModelConstructor()
{
New = new MyCommand((parameter) =>
{
//Add new object
});
Delete = new MyCommand((parameter) =>
{
//Delete object
});
ClearAll = new MyCommand((parameter) =>
{
//Clear all objects
});
}

Related

WPF How to load Items inside ContextMenu without open it?

I try to get MenuItem from ContextMenu. If I open context menu at least once it works fine, otherwise I get error ItemCollection has no inner collection. As I understand this behavior is caused by ItemSource binding. How can I load Items without manual open context menu?
XAML:
<StackPanel>
<Rectangle Fill="Red" Height="100" Width="100">
<Rectangle.ContextMenu>
<ContextMenu x:Name="MainContextMenu" ItemsSource="{Binding Items}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Rectangle.ContextMenu>
</Rectangle>
<Button Content="Click" Command="{Binding GetMenuItemCommand}"
CommandParameter="{Binding ElementName=MainContextMenu}"
Height="30" Width="100"/>
</StackPanel>
ViewModel:
public class ViewModel
{
public List<string> Items { get; }
public MyCommand GetMenuItemCommand { get; }
public ViewModel()
{
Items = new List<string>()
{
"a0", "a1", "a2", "a3", "a4", "a5"
};
GetMenuItemCommand = new MyCommand(GetMenuItem);
}
public void GetMenuItem(object obj)
{
var contextMenu = (ContextMenu)obj;
// Here I get exception
var item = contextMenu.Items[0] as MenuItem;
var header = item.Header
}
}
MyCommand
public class MyCommand : ICommand
{
private Action<object> execute;
public MyCommand(Action<object> execute)
{
this.execute = execute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter)
{
execute?.Invoke(parameter);
}
}
NOTE: It is demo code, I'm not following MVVM approach here
EDIT: Full exception message: System.InvalidOperationException: 'Operation is not valid while ItemCollection has no inner collection. ItemCollection is uninitialized or binding on ItemsControl.ItemSource supplied null for collection.'
EDIT 2: As people mentioned in the comments, probably purpose of getting MenuItem is not clear, so I introduce more details.
I'm trying to write helper class, that will merge child context menu with parent menu if needed. I do not know anything about context menus I will merge. I am currently trying to copy elements from parent menu and add them to child menu, when child menu is opening. For that reason I need to create new MenuItem, copy properties from parent's MenuItem and add this new MenuItem to child's context menu. My solution works fine, but only when parent menu was opened at least once.
I already asked straight question about merging context menus, but got no appropriate answers, so there I tried to ask something different.
There is no MenuItem until the ContextMenu has been opened. And passing a ContextMenu to a view model breaks the MVVM pattern.
You should get the underlying data value from the source collection instead of trying to access the eventually created visual element. The MVVM version of your code looks something like this:
public void GetMenuItem(object _)
{
var item = Items[0];
...
}
Obviously a view model cannot create additional MenuItem elements. It may add more strings to the source collection though.

Is it possible to bind the MenuItem.IsEnabled property to a different Context?

I have a ListView bound to a collection of objects (called Users, in this case), and the template includes a ContextActions menu. One of the menu items needs to be enabled or disabled depending on a condition having nothing directly to do with the items in the view (whether or not there's a Bluetooth connection to a certain kind of peripheral). What I'm doing right now is iterating the Cells in the TemplatedItems property and setting IsEnabled on each.
Here's the XAML for the ListView, stripped down to the parts that matter for my question:
<ListView ItemsSource="{Binding .}" ItemTapped="item_Tap">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Label}">
<TextCell.ContextActions>
<MenuItem
Text="Copy to other device"
ClassId="copyMenuItem"
Clicked="copyMenuItem_Click" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Here's how I'm setting the property values now:
foreach (Cell cell in usersListView.TemplatedItems)
{
foreach (MenuItem item in cell.ContextActions)
{
if ("copyMenuItem" == item.ClassId)
{
item.IsEnabled = isBluetoothConnected;
}
}
}
That works, but i don't like it. It's obviously out of line with the whole idea of data-bound views. I'd much rather have a boolean value that I can bind to the IsEnabled property, but it doesn't make sense from an object design point of view to add that to the User object; it has nothing to do with what that class is about (representing login accounts). I thought of wrapping User in some local class that exists just to tape this boolean property onto it, but that feels strange also since the value will always be the same for every item in the collection. Is there some other way to bind the MenuItem.IsEnabled property?
Use relative binding
Get ready in your view model class, inherit INotifyPropertyChanged or your BaseViewModel.
public class YourViewModel : INotifyPropertyChanged
{
private string isBluetoothConnected;
public string IsBluetoothConnected
{
get => isBluetoothConnected;
set => SetProperty(ref isBluetoothConnected, value);
}
public ObservableCollection<User> Users { get; private set; }
}
Add a name to ListView for reference, and apply relative binding in MenuItem.
<ListView
x:Name="UserListView"
ItemsSource="{Binding Users}"
ItemTapped="item_Tap">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Label}">
<TextCell.ContextActions>
<MenuItem
IsEnabled="{Binding Path=BindingContext.IsBluetoothConnected, Source={x:Reference UserListView}}"
Text="Copy to other device"
ClassId="copyMenuItem"
Clicked="copyMenuItem_Click" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
It turns out that this case of BindableProperty is, in fact, not bindable: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/menuitem#enable-or-disable-a-menuitem-at-runtime
One must add a Command property to the MenuItem and assign a BindingContext to that, and set its executability. Here's the latest version of my code, which does work:
<MenuItem
Text="Copy to other device"
Clicked="copyMenuItem_Click"
BindingContext="{x:Reference usersListView}"
Command="{Binding BindingContext.CopyCommand}" />
public class UsersViewModel
{
public Command CopyCommand { get; set; }
public bool IsBluetoothConnected
{
get { return isBluetoothConnected; }
set
{
isBluetoothConnected = value;
if (CopyCommand.CanExecute(null) != value)
{
CopyCommand.ChangeCanExecute();
}
}
}
public ObservableCollection<User> Users { get; private set; }
private bool isBluetoothConnected;
public async System.Threading.Tasks.Task<int> Populate( )
{
CopyCommand = new Command(( ) => { return; }, ( ) => IsBluetoothConnected); // execute parameter is a no-op since I really just want the canExecute parameter
IList<User> users = await App.DB.GetUsersAsync();
Users = new ObservableCollection<User>(users.OrderBy(user => user.Username));
return Users.Count;
}
}
I'm still not entirely happy with this; it contaminates the view model with the concerns of a specific view. I'm going to see if I can separate the Command from the view model. But it does accomplish my primary goal, bringing this UI implementation into the data binding paradigm.

How does databinding between ContextMenuItems of a DataGridRow and the DataGridRow itself to properties within encapsulated Viewmodel works?

I know I really should start reading a book about XAML and WPF because I think all my Problems here belong to a lack of understanding about Data Binding (I used WinForms for years):
My Application consists of a TreeView and a DataGrid.
In the TreeView I have added ViewModels for each ParentNode, ChildNode an GrandChildNode.
I've used the sample from Josh Smith found here.
To be short, he/I used
<HierarchicalDataTemplate
DataType="{x:Type local:ViewModel.TreeViewChildNodeViewModel}"
ItemsSource="{Binding Children}">
</HierarchicalDataTemplate.Resources>
to bind the ChildNode to a ChildNodeViewModel and to the corresponding Model.
I than added - in the TreeViewChildNodeViewModel constructor:
ContextMenuItems = new List<MenuItem>();
ContextMenuItems.Add(new MenuItem() {
Header = "Click",
Command = _cmdDoSmth
ToolTip = new ToolTip() { Content = "blabla" }
}
);
which is exposed to the View with this property:
private readonly List<MenuItem> ContextMenuItems;
public List<MenuItem> ContextMenu {
get { return ContextMenuItems; }
}
Note that, I have multiple constructors. I add different ContextMenuItems to the ContextMenu List depending on what Model i want the ViewModel to work with. The "root" ChildNode consist of a:
<TextBlock
Text="{Binding ChildNodeDisplayItem}">
<TextBlock.ContextMenu>
<ContextMenu
ItemsSource="{Binding ContextMenu}"></ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
That works like it should. Now my problems start with trying to do some similar with the datagrid.
What I need to achieve is:
I'd like to show rows in the datagrid. Each Row has its own Viewmodel with an exposed List of ContextMenuItem's (as well as the model of course). I'd like to be able to define the count, header and command of each contextmenuitem in dependence of the viewmodel that is selected.
What I did so far:
In my MainWindow.xaml:
<Controls:MetroWindow.Resources>
<ContextMenu x:Key="DataRowContextMenu" ItemsSource="{Binding Path=ActionReactionDataGridViewModel/ContextMenu, RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}}"/>
</Controls:MetroWindow.Resources>
<DataGrid
AutoGenerateColumns="True"
AutoGeneratingColumn="OnAutoGeneratingColumn"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="1,1,1,1"
Margin="0,0,0,0"
ItemsSource="{Binding Path=ActionReactionDataGridViewModel/DataGridSource}"
SelectedItem="{Binding Path=ActionReactionDataGridViewModel/SelectedDataGridItem}"
BorderBrush="#FF020202">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="ContextMenu" Value="{StaticResource RowMenu}" /> </Style>
<DataGrid.RowStyle>
</DataGrid>
In my MainWindowViewModel:
public MainWindowViewModel() // Constructor
{
actionReactionDataGrid = new ObservableCollection<ActionReactionDataGridViewModel>();
actionReactionDataGrid.Add(new ActionReactionDataGridViewModel());
}
private ObservableCollection<ActionReactionDataGridViewModel> actionReactionDataGrid;
public ObservableCollection<ActionReactionDataGridViewModel> ActionReactionDataGridViewModel
{
get { return actionReactionDataGrid; }
}
My ActionReactionDataGridViewModel is here:
public class ActionReactionDataGridViewModel : ViewModelBase
{
private readonly List<MenuItem> ContextMenuItems;
public ActionReactionDataGridViewModel()
{
ContextMenuItems = new List<MenuItem>();
ContextMenuItems.Add(new MenuItem()
{
Header = "blubb"
});
dataGridSource = new ObservableCollection<ActionReactionDataGridModel>();
dataGridSource.Add(new ActionReactionDataGridModel("Status","Eventname","Eventtyp","ReaktionName","ReaktionTyp"));
}
public List<MenuItem> ContextMenu {
get { return ContextMenuItems; }
}
private ActionReactionDataGridModel selectedDataGridItem;
public ActionReactionDataGridModel SelectedDataGridItem {
get { return selectedDataGridItem; }
set {selectedDataGridItem = value; RaisePropertyChanged("SelectedDataGridItem"); }
}
private ObservableCollection<ActionReactionDataGridModel> dataGridSource;
public ObservableCollection<ActionReactionDataGridModel> DataGridSource {
get { return dataGridSource; }
set { dataGridSource = value; RaisePropertyChanged("DataGridSource"); }
}
}
I think posting the content of the model is not neccessary because it just contains the column headers and some sample strings. I think what iam missing is the knowledge of telling the DataGrid Control in the View in MainWindow.xaml to bind the itemssource to "DataGridSource" instead of "ActionReactionDataGridViewModel".
I found other posts on SO about adding Context Menus to a datagridrow. what i was missing is the ability to bind the count, text and command to each viewmodel.
Any Help would be greatly appreciated.
Thank you.
// EDIT 1
Ok. finding out how to pass the property of a viewmodel from inside a collection of viewmodels was easy.
i added
ItemsSource="{Binding Path=ActionReactionDataGridViewModel/DataGridSource}
explanation is here
Now I "just" need to figure out how to add the contextmenu items to each viewmodel...
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="HeaderName">
</MenuItem>
</ContextMenu>
</DataGrid.ContextMenu>
inside menu item you can write your control.

Xamarin.Forms and binding for ElementName

I'm trying to bind my ListView Item to Command from my parent ViewModel. The problem is that I would like to add CommandParameter with current Item
Basically, in WPF i would do something like
<MyItem Command="{Binding ElementName=parent", Path=DataContext.MyCommand}" CommandParameter="{Binding}"/>
In Xamarin Forms ElementName is not working, so the way is to use BindingContext, but how I should use it (if one of my binding points to parent, second one to self)?
I have tried
<MyItem Command="{Binding BindingContext.RemoveCommand, Source={x:Reference parent}}" CommandParameter="{Binding }" />
But it does not work (seems it not changing Source).
I know, that with normal binding a way is to use BindingContext="{x:Reference parent}", but it won't work in this example, because I need Self binding for CommandParameter
How can i do it?
I understand you want to execute a command on the parent node of your current node but pass the current node as a parameter. If that is the case, you can solve it like this:
Here's the model we bind to. It has a Parent property and it defines an ICommand (please note that all is C#6 code, so you'll need XS or VS2015!):
public class BindingClass
{
public BindingClass (string title)
{
this.TestCommand = new TestCommandImpl (this);
this.Title = title;
}
public string Title { get; }
public ICommand TestCommand { get; }
public BindingClass Parent { get; set; }
}
In the code behind, the binding context is set:
this.BindingContext = new BindingClass ("Bound Button") {
Parent = new BindingClass ("Parent")
};
And in the XAML, we have a button that calls the command of the parent node and passes the current node as a parameter:
<Button
x:Name="btnTest"
Text="{Binding Title}"
Command="{Binding Parent.TestCommand}"
CommandParameter="{Binding}"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"/>

How to bind a command to StackPanel or Grid tap event?

I have seen some answers regarding WP8 or others, however it seems that there is no triggers in WP8.1 (Or I am missing something?)
I have a datatemplate bound from the code (it is a hub datatemplate, and I have a mix of static and dynamic hubsections, therefore this datatemplate needs to be set from the code).
This datatemplate is defined in a separate xaml file, it includes a listbox (or listview) with another datatemplate defined for the items.
I need to bind a command on the item's tap or listbox selectionchanged (or something equivalent). However, the tap event defined in the template is not called, therefore I thought of binding a command on an UI element, but these seems not to support Commands neither interactivity triggers.
Any clue on how to handle that? :)
On the example below I don't get the event Item_Tapped nor ListBox_SelectionChanged, I would anyway prefer to bind one of these to a command in the viewmodel.
<DataTemplate x:Key="HubSectionTemplate">
<Grid>
<ListBox ItemsSource="{Binding MyNodes}"
SelectionChanged="ListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="64" Tapped="Item_Tapped" >
<TextBlock Text="{Binding MyText}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</DataTemplate>
This is how it is used from code:
HubSection hs = new HubSection()
{
ContentTemplate = Application.Current.Resources[HUBSECTION_TEMPLATE] as DataTemplate,
DataContext = model,
Tag = model.UniqueId,
};
Hub.Sections.Insert(firstSectIdx + 1, hs);
public class Model
{
public Guid UniqueId {get;set;}
public List<ItemModel> MyNodes {get;set;}
}
public class ItemModel
{
public string MyText {get;set;}
}
PS: The ItemModel is defined in another assembly and therefore should not be edited (the command should be in the Model class if possible)
--- EDIT ---
In order to simplify the problem, I use the following models:
public class Model
{
public Guid UniqueId {get;set;}
public List<ItemModel> MyNodes {get;set;}
public ICommand MyCommand {get;set;}
}
public class ItemModel
{
Model _Model;
public ItemModel(Model m) {_Model = m; }
public string MyText {get;set;}
public ICommand MyCommand { get { return _Model.MyCommand; }}
}
And my (temporary) solution is to use a button in the itemtemplate:
<ListView.ItemTemplate>
<DataTemplate>
<Button HorizontalAlignment="Stretch" Command="{Binding TapCommand}" Height="64">
<TextBlock Text="{Binding MyText}" />
</Button>
</DataTemplate>
</ListView.ItemTemplate>
You can use Behaviors SDK.
In Visual Studio go to 'Tools -> Extension and updates' and install Behaviors SDK (XAML). Then reference it in your project using Add reference dialog.
After that add following namespaces to your page:
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
Now you can register events like tap on your stack panel using following syntax:
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="64">
<TextBlock Text="{Binding MyText}" />
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Tapped">
<core:InvokeCommandAction Command="{Binding YourCommand}"/>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</StackPanel>
</DataTemplate>
However this code only works if your Command is defined in your ItemModel class. If you want to bind to the parent element Command, you can try something like this (not tested):
{Binding ElementName=LayoutRoot, Path=DataContext.ParentCommand}
But I would preferer having command on your ItemModel class
Edit: Solution without Behaviors SDK:
If you are using ListView (or something inherited from ListViewBase) you can use ItemClick event. To make it more reusable and Mvvm friendly you can implement your DependencyProperty like this:
public static class ItemClickCommand
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand),
typeof(ItemClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, ICommand value)
{
d.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject d)
{
return (ICommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
{
control.ItemClick += OnItemClick;
}
}
private static void OnItemClick(object sender, ItemClickEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e.ClickedItem))
{
command.Execute(e.ClickedItem);
}
}
}
Then your ListView will look like this:
<ListView
IsItemClickEnabled="True"
helpers:ItemClickCommand.Command="{Binding YourCommand}"
ItemsSource="{Binding MyNodes}"
ItemTemplate="{StaticResource YourDataTemplate}" />
In this case your child item is passed to your command as a parameter, so it should also solve your problem with your Command defined in parent model.

Categories

Resources