Misbehaving context menu (mvvm) - c#

I've put together what I thought was a context menu in an MVVM setting (I'm using WPF with XAML and C#, using MVVM). Only it's not working, which is why I'm here. I'm getting nothing in my context menu.
The XAML is supposed to call an ICommand in the code behind (or Relay Command since I'm using micro MVVM - same thing basically).
The first thing was to set up an object which the XAML could get the two needed values from - the Header and the Command. The item in question looks like this:
class ContextMenuVM : ObservableObject
{
public string Displayname { get; set; }
public RelayCommand ContextMenuCommand { get; set; }
}
So, rather simple there. These will be used for the bindings in the menu.
The view model here is called 'CharacterListViewModel' and contains an ObservableCollection if these ContextMenuVM objects. That looks like this:
private ObservableCollection<ContextMenuVM> _sceneAddMenu = new ObservableCollection<ContextMenuVM>();
public ObservableCollection<ContextMenuVM> SceneAddMenu
{
get { return _sceneAddMenu; }
set
{
if (_sceneAddMenu != value)
{
_sceneAddMenu = value;
RaisePropertyChanged("SceneAddMenu");
}
}
}
The ObservableCollection is populated, as follows:
foreach (Scene s in Database.Instance.Scenes)
{
SceneAddMenu.Add(new ContextMenuVM()
{
Displayname = s.SceneName, ContextMenuCommand = new RelayCommand(
() =>
{
MessageBox.Show("Clicked");
})
});
}
Just a test at the moment, but I can say through use of break points that SceneAddMenu contains four items after this code is run (as I would expect).
Well, that's kind of the background code. I suspect it works, although clearly something is broken. My suspicion is the XAML.
The context menu code itself is here:
<ContextMenu x:Key="CharacterMenu" ItemsSource="{Binding SceneAddMenu}">
<ContextMenu.ItemTemplate >
<DataTemplate DataType="MenuItem">
<MenuItem Header="Edit" Command="{Binding ContextMenuCommand}"></MenuItem>
</DataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
Ah, so the obvious problem would be that the data context is not properly set up. Well that's not the case because this context menu replaces another one which utilised a command in the view model (and that worked), so my assumption is that the view model is okay.
For the record, the previous context menu, which works, looked like this:
<ContextMenu x:Key="CharacterMenu">
<MenuItem Header="Edit" Command="{Binding EditCharacter}"/>
</ContextMenu>
And if I put that back in, it works. Since it has a binding to the view model, that would suggest that the data context is not the problem.
The Context menu itself is referenced a little later, like this:
<StackPanel Orientation="Horizontal" Margin="3" ContextMenu="{StaticResource CharacterMenu}">
But since that was also in before with the previous menu (i.e. when it worked), I only include it for completion's sake.
So the SceneAddMenu object (ObservableCollection) is populated. That seems to be fine. Somewhere between the XAML and the view model there must be a problem though. If I put a break point in the 'get' for SceneAddMenu and then right click on the item in question, the break point does not activate.
I am at a bit of a loss on this one. It's my first time creating a context menu using the MVVM method, so it's possible I missed out a step somewhere.
If you read all of this, thanks a lot. If I missed out any information, please let me know.

You shouldn't add a MenuItem to the ItemTemplate of a ContextMenu. You should define an ItemContainerStyle and bind to the Displayname and ContextMenuCommand properties of your class:
<ContextMenu x:Key="CharacterMenu" ItemsSource="{Binding SceneAddMenu}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Displayname}" />
<Setter Property="Command" Value="{Binding ContextMenuCommand} " />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>

Related

WPF: Reusing control with slightly different call on ViewModel?

I have a control, ActionRequiredControl.xaml that includes a combo-box with a list of enums. This control has ActionRequiredViewModel as it's DataContext, and the following call is used to populate the combobox:
public IEnumerable<ActionType> Actions
{
get
{
return Enum.GetValues(typeof(ActionType)).Cast<ActionType>();
}
}
And the combo-box is as follows.
<StackPanel Orientation="Vertical">
<Label>Action:</Label>
<ComboBox Width="170" MinHeight="45" Margin="5,0,5,5" Padding="5"
SelectedItem="{Binding Action, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Actions, Mode=OneTime}"
VerticalContentAlignment="Center" IsEnabled="{Binding IsUsed}"/>
</StackPanel>
This control is used in two places currently (There is more to it than just the combobox) - however, I would like to re-use it in a third place, only have the "Actions" function return a different list of .
What's the best way to go about this? The only thing I can think of is creating a new class that inherits from ActionRequiredViewModel and overrides the Actions method.
The simplest solution was the one I indicated in the question - I created a new class that inherited from ActionRequiredViewModel and overrode the "Actions" method to return a different list of enums.
Yes, creating an additional ViewModel will be the simplest solution.
I was in a similar situation some time ago, my first implementation (using MVVM light) was using two VMs, and registering both in the ViewModelLocator and providing a property for each:
SimpleIoc.Default.Register<ViewModel1>();
SimpleIoc.Default.Register<ViewModel2>();
public ViewModel ViewModel1Instance
{
get
{
return ServiceLocator.Current.GetInstance<ViewModel1>();
}
}
public ViewModel ViewModel2Instance
{
get
{
return ServiceLocator.Current.GetInstance<ViewModel2>();
}
}
And then in XAML:
<local:MyUserControl DataContext="{Binding ViewModel1Instance,
Source={StaticResource Locator}}" ...
However, to be a bit more flexible, I replaced this with another solution later, which provided better customization.
I played a bit with your requirements and came to this solution:
Creating a dependency property in the code behind of the user control as a proxy:
public static readonly DependencyProperty ComboBoxItemsProperty =
DependencyProperty.Register("ComboBoxItems",
typeof(IEnumerable<object>), typeof(TestUserControl));
public IEnumerable<object> ComboBoxItems
{
get
{
return (IEnumerable<object>)GetValue(ComboBoxItemsProperty);
}
set
{
SetValue(ComboBoxItemsProperty, value);
}
}
In the XAML of the user control:
...ItemsSource="{Binding ComboBoxItems, Mode=OneWay, RelativeSource=
{RelativeSource AncestorType={x:Type UserControl}}}"...
In the main view model:
public IEnumerable<object> Items
{
get
{
return Enum.GetValues(typeof(ActionType)).Cast<object>();
}
}
In the main XAML:
<local:TestUserControl ComboBoxItems="{Binding DataContext.Items,
Mode=OneTime,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
</local:TestUserControl>
This worked for me, but using object instead of the custom type is not the most elegant solution.
PS: This is my very first answer on SO, (and no questions yet from me...).
Since I used SO for many years as my primary source of tips and tricks, I thought I can TRY to give something back...
And of course, I admit: trying to answer is not the worst method of learning a new topic (and WPF is fairly new to me, so please donĀ“t blame me for a suboptimal answer :-)

How to bind a button on wpf grid to a method on MVVM when I am using caliburn micro

I have a Grid on a wpf window which I want to add the capability that user can delete some of the items by clicking on a delete button. The application uses Calibrun Micro to bind view to ViewModel.
My question?
1- Is it a good idea to use a button to delete an item from a grid in WPF?
2- How can I bind a button to a method on VM and in the methd get a pointer to the item that should be deleted?
Edit1
I added the buttons in this way to datagrid:
<DataGridTemplateColumn Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" cal:Message.Attach="DeleteFromList($dataContext)" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
and c# code follow:
public void DeleteFromList(object tmp)
{
}
But the buttons on datagrid are disabled and clicking on them doesn't fire DeleteFromList method (I checked using debugger).
Why they are disabled? How can I make them enabled?
This depends on how your button is placed - is there a single 'delete' button or have you added a button per row in the grid (are we talking DataGrid or just Grid?)
Assuming you are talking about DataGrid, you can easily just add an action message command to the button and pass through the item which is being deleted to the message handler on the VM
e.g. in the VM
public class MyViewModel
{
public DataItemCollectionTypeName ItemCollection { get; set; }
public void DeleteItem(DataItemTypeName item)
{
ItemCollection.Remove(item);
}
}
Assuming ItemCollection is bound to the grid, the button XAML may look like this:
<Button cal:Message.Attach="[Click] = [DeleteItem($datacontext)]" />
You may also need to set Action.TargetWithoutContext (it should be bound to the VM) if this is a templated row, as otherwise CM will not be able to locate the VM to invoke the action message on
If you have a single button that isn't contained within the grid you can always target the grids SelectedItem in the action message
<DataGrid x:Name="SomeDataGrid"></DataGrid>
<Button cal:Message.Attach="[Click] = [DeleteItem(SomeDataGrid.SelectedItem)]" />
It may be (and probably is) the default property that CM will look at so you may not need to specify the property name unless you have modified default conventions
<DataGrid x:Name="SomeDataGrid"></DataGrid>
<Button cal:Message.Attach="[Click] = [DeleteItem(SomeDataGrid)]" />
Edit
To clarify: In order for CM to find a VM to call the DeleteItem method it uses the DataContext of the current item. In the case of an ItemsControl derived control, the datacontext for each item points to the item being bound, not the ViewModel.
In order to give CM a hint as to which object it should try to resolve the DeleteItem method on, you can use the Action.TargetWithoutContext attached property, which applies a target object for action messages without changing the DataContext of the bound row/item
You can use element name syntax to point to the correct place:
In this example I've used a grid as the root element and named it LayoutRoot, then I've pointed the action message target to LayoutRoot.DataContext (which will be the ViewModel) using ElementName syntax. You can use any method (AncestorType or whatever)
<Grid x:Name="LayoutRoot">
<DataGridTemplateColumn Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" cal:Message.Attach="DeleteFromList($dataContext)" cal:Action.TargetWithoutContext="{Binding DataContext, ElementName=LayoutRoot}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</Grid>
That should then work!
You could do something like this...
<Button cal:Message.Attach="[Event MouseEnter] = [Action Save($this)]">
Check the docs as they will explain what you need to do and should answer your question: link

Back-button gets focused on list manipulation

I am creating a ListView that can have some items removed from it (it's a list of favorites) by selecting them, bringing up the app bar and clicking "Remove from favorites". When the button is clicked, a method in the current view model is asked to remove this item from the list. After this happens, the UI gets updated, and the item is removed.
Now, I have two problems. The first one is that the back-button of the page receives the focus (it gets a dotted outline) when an item is removed, something which I do not want.
The second problem is that the list doesn't use the add / delete animation I've set it to use.
A solution to either of these would be appreciated.
Here is some pseudo code showing what happens:
XAML:
<GridView x:Name="FavoritesGridView"
Grid.Row="1"
SelectionMode="Multiple"
ItemTemplate="{StaticResource FavoritesOnSectionViewItemTemplate}"
ItemsSource="{Binding FavoritesList}"
ItemClick="ProgramGrid_OnItemClick"
IsItemClickEnabled="True"
SelectionChanged="FavoritesGridView_OnSelectionChanged"
ScrollViewer.HorizontalScrollMode="Disabled">
<GridView.ItemContainerStyle>
<Style TargetType="Control">
<Setter Property="Margin" Value="0,0,38,8"/>
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemContainerTransitions>
<TransitionCollection>
<AddDeleteThemeTransition/>
</TransitionCollection>
</GridView.ItemContainerTransitions>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid Orientation="Vertical" MaximumRowsOrColumns="9" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</GridView>
Codebehind:
private void UnFavoriteButton_Click(object sender, RoutedEventArgs e)
{
viewModel.RemoveFromFavorites(FavoritesGridView.SelectedItems.Cast<FavoriteProgram>().AsEnumerable());
}
ViewModel:
public void RemoveFromFavorites(IEnumerable<FavoriteProgram> programs)
{
FavoriteController.RemoveFromFavorites(programs);
UpdateUi();
}
private void UpdateUi()
{
OnPropertyChanged("FavoritesList");
}
public IEnumerable<FavoriteProgram> FavoritesList
{
get { return CoreData.TvFavorites; } // A centralized list
}
FavoritesController:
public static void RemoveFromFavorites(IEnumerable<FavoriteProgram> programs)
{
if (programs.IsNullOrEmpty()) return;
foreach (var program in programs)
RemoveFromFavorites(program);
}
public static void RemoveFromFavorites(FavoriteProgram program)
{
if (!IsFavorite(program)) return;
var list = CoreData.TvFavorites.ToList();
list.Remove(program);
CoreData.TvFavorites = list.AsEnumerable();
}
Any ideas?
I see. So you have two problems.
[1]. The back button receives focus.
It is my opinion that the Back button should never receive focus. There is already a key gesture to go back, so setting focus is silly. Why it wasn't already disabled to have focus, I do not know. Here's all you do:
<Button TabIndex="-1" Style="{StaticResource BackButtonStyle}" />
Or you can do it with a style:
<Grid Background="Black">
<Grid.Resources>
<Style TargetType="Button" BasedOn="{StaticResource BackButtonStyle}" x:Name="MyBackButtonStyle">
<Setter Property="TabIndex" Value="-1" />
</Style>
</Grid.Resources>
<Button Style="{StaticResource MyBackButtonStyle}" />
</Grid>
Using this new style (or just updating the existing one) will result in the back button never receiving focus. If you want it to be able to receive focus, for some reason, then the solution would be to handle the GotFocus event and simply use (sender as Button).Focus(FocusState.Unfocused);. To be fair, you should also determine why you would be removing focus.
[2]. The animations are not happening
This is a common problem. The reason is, you do not want to setup animations on the ListView, you want to set up the animations on the ListView ItemsPanel. Here's all you want to do:
<ListView>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel>
<StackPanel.ChildrenTransitions>
<AddDeleteThemeTransition />
</StackPanel.ChildrenTransitions>
</StackPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
It's as simple as that. (my sample is a StackPanel, remember to use a WrapGrid as you have in your code) You just had the transitions in the wrong place. So, now you can handle the focus problem you are having and you can get the transitions you want.
I might offer a bit of advice. Since you are using view models, it sounds strange to hear that you are not also using delegate commands. If you want to use MVVM at its best, delegate commands solve a lot of problems for you. Read here: http://blog.jerrynixon.com/2012/08/most-people-are-doing-mvvm-all-wrong.html
And for a second bit of advice. It sounds to me like you might be using the default templates from Visual Studio. Most developers start there. The problem is that those templates are not very great to teach best practices. My suggestion: don't be afraid of the blank template.
Best of luck!
Now, I have two problems. The first one is that the back-button of the
page receives the focus (it gets a dotted outline) when an item is
removed, something which I do not want.
This problem could be solved, by adding a TwoWay-Binding to the SelectedItem property of the GridView. After removing the favorite programms set the SelectedItem per code, so that it gets focused in the GridView.
XAML:
<GridView x:Name="FavoritesGridView"
SelectedItem="{Biding SelectedFavorite, Mode=TwoWay}" />
C#:
private FavoriteProgram _selectedFavorite;
public FavoriteProgram SelectedFavorite {
get {
return _selectedFavorite;
}
set {
_selectedFavorite = value;
OnPropertyChanged("SelectedFavorite");
}
}
After removing your items, set the property SelectedFavorite to an item in your FavoritesList.
public void RemoveFromFavorites(IEnumerable<FavoriteProgram> programs) {
FavoriteController.RemoveFromFavorites(programs);
UpdateUi();
SelectedItem = FavoritesList.FirstOrDefault(); // selects the first element in list.
}
The second problem is that the list doesn't use the add / delete
animation I've set it to use.
The problem here is that you always use a new collection/list for your property CoreData.TvFavorites after you have removed your favorites and therefore can the GridView not determine which items have been removed or added.
For binding scenarios there is a specialized collection named ObservableCollection<T>, that implements the interface INotifyCollectionChanged. The interface defines an event to notify (UI Elements) that items are added or removed from the collection. You should change your property FavoritesList to type ObservableCollection<FavoriteProgramm> and update the collection in your UpdateUI method to remove the relevant favorites.
private void UpdateUi()
{
//Update your FavoritesList to enable animations.
OnPropertyChanged("FavoritesList");
}
private ObservableCollection<FavoriteProgram> _favorites;
public ObservableCollection<FavoriteProgram> FavoritesList
{
get {
if (_favorites == null) {
_favorites = new ObservableCollection<FavoriteProgram>();
}
return _favorites;
}
}

Sorting a bound observable collection

I have an MVVM collection that I "know" is reordered in the VM but not showing in it's new order in the view. Given code similar to that below, should I expect the the list to re-display in a new sort without manipulating the CollectionViewSource?
xaml
<Menu Name="_mainMenu" Height="22" >
<MenuItem Header="Language"
ItemsSource="{Binding AvailableCultures}" >
<MenuItem.ItemTemplate>
<DataTemplate>
<MenuItem IsCheckable="True"
IsChecked="{Binding IsSelected, Mode=TwoWay}"
Header="{Binding DisplayName}"/>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
</Menu>
vm
public ObservableCollection<OptionLocalizedViewModel<CultureInfo>>
AvailableCultures { get; private set; }
private void OnSelectionChange(OptionLocalizedViewModel<CultureInfo> option)
{
...
var sorted = AvailableCultures.OrderBy(x => x.DisplayName);
AvailableCultures =
new ObservableCollection<OptionLocalizedViewModel<CultureInfo>>(sorted);
NotifyOfPropertyChange(() => AvailableCultures);
}
UPDATE
The order is being changed, but not as expected (and not what the debugger shows the newly sorted ObsCollection to be). I also tried ditching the ObsCollection in favor of binding to an IEnumerable directly with the exact same result.
Does anyone see a pattern that suggests a fix??
1) initial load, looks as it should
2) select Spanish, so should be Espanol first but isn't
3) back to English, but somehow English is last. How did this get flipped?
4) back to Spanish, same as try (2)
Try using ListCollectionView instead :
ListCollectionView LCV = new ListCollectionView(YourObservableCollection);
LCV.GroupDescriptions.Add(new PropertyGroupDescription("PropertyName"));
YourDataBoundProperty = LCV;
You can refer to this article for more detail.
This should theoretically work, just be sure INotifyPropertyChanged is actually getting fired correctly as it is necessary when replacing the entire collection with a different one rather than just altering it's contents.

WPF context menu bound to List<> dependency property

Im trying to make the contents of a List thats a dependency property show up in a WPF context menu.
I have a class with the following dependency property, a list of Foo's (data holding class):
public List<Foo> FooList
{
get { return (List<Foo>)GetValue(FooListProperty); }
set { SetValue(FooListProperty, value); }
}
public static DependencyProperty FooListProperty =
DependencyProperty.Register("FooList", typeof(List<Foo>),
typeof(FooButton));
In XAML I set up the following static resource, I assume its needed since the context menu isnt part of the visual tree:
<UserControl.Resources>
<ResourceDictionary>
<CollectionViewSource
x:Key="FooListSource"
Source="{Binding FooList}"/>
<!-- ... -->
</ResourceDictionary>
</UserControl.Resources>
Also part of the ResourceDictionary above is a CompositeCollection which is needed to make the items show up in the actual context menu. If the UserControl CanStop property is true, we also show a separator and a stop command. These bindings does also fail, although the MenuItems themselves show up. So If I can figure out why these fail, the List might be easier.
<CompositeCollection x:Key="FooListItems">
<CollectionContainer
Collection="{Binding Source={StaticResource FooListSource}}"/>
<Separator
Visibility="{Binding CanStop,
Converter={StaticResource VisibleIfTrue}}" />
<MenuItem
Command="{x:Static Buttons:FooButton.Stop}"
Header="Stop"
Visibility="{Binding CanStop,
Converter={StaticResource VisibleIfTrue}}"/>
</CompositeCollection>
And finally the context menu itself, also in the ResourceDictionary:
<ContextMenu
x:Key="FooButtonMenu"
ItemsSource="{Binding Source={StaticResource FooListItems}}"
ItemTemplate="{StaticResource FooListTemplate}"
<ContextMenu.CommandBindings>
<CommandBinding
Command="{x:Static Buttons:FooButton.Stop}"
Executed="Stop_Executed" />
</ContextMenu.CommandBindings>
</ContextMenu>
I feel Im posting way to much code but Im not sure I can make this piece any simpler. Only the separator and the hardcoded menuitem shows up. So something must be messed up with the bindings. Bindings are usually not that hard but now when I want to bind something thats not really part of the same tree I feel a bit lost.
Any suggestions are welcome. :)
As you suspected, your problem does seem to be caused by the use of List<Foo> instead of ObservableCollection<Foo>. Since List<Foo> doesn't notify on property changes, the only way to get WPF to recognize you've added or removed an item is to temporarily set the FooList property to something else and then set it back.
There is no need to switch to a CLR property. Just change List<Foo> to ObservableCollection<Foo>.
The reason the bindings in your CompositeCollection aren't working is that CompositeCollection is not a DependencyObject, so it can't inherit a DataContext.
I don't see why you've made FooList a dependency property. You're not making it the target of a binding, which is the most common reason to create a dependency property. You haven't implemented a callback, so it can't do change notification (the second most common reason to create a dependency property). You're not using it for value inheritance. So why, then?
It seems to me that what you really want is for FooList to be a normal CLR property of type ObservableCollection<Foo> (or any class that implements INotifyCollectionChanged). That will do all the change notification that you need - at least, that you need for the code you've posted so far.

Categories

Resources