Partially filling a MenuItem through binding - c#

I want to use a list of custom objects to generate a list of MenuItems in a menu, but at the bottom of that menu I want a couple of static MenuItems to always appear. Logically in my head, this can be done by programatically creating another list to bind to that always has those MenuItems at the bottom, but this strikes me as a bit of a naive way of approaching this. I'm sure there's a more elegant way to do it with the list I already have along with some XAML wizardry, possibly with some kind of DataTemplate. Any pointers?

Like McGarnagle said, you can use a CompositeCollection. However, you don't need to create a resource for the fixed menu items. You can place them directly in the CompositeCollection like this:
<Menu>
<Menu.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Path=MyItems}" />
<Separator/>
<MenuItem Header="Fixed item 1" />
<MenuItem Header="Fixed item 2" />
</CompositeCollection>
</Menu.ItemsSource>
</Menu>

Use a CompositeCollection, with two child collections (the generated menu items, and the static ones).
Edit Should look something like this:
<Button Content="Test">
<Button.Resources>
<viewModel:MenuItemCollection x:Key="FixedMenuItems">
<MenuItem Header="Fixed Item" />
</viewModel:MenuItemCollection>
</Button.Resources>
<Button.ContextMenu>
<ContextMenu>
<ContextMenu.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{StaticResource FixedMenuItems}" />
<CollectionContainer Collection="{Binding MyMenuItems}" />
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>
</Button.ContextMenu>
</Button>
Where viewModel:MenuItemCollection is just a list of MenuItem:
public class MenuItemCollection : ObservableCollection<MenuItem>
{
}
Second Edit
One fix is needed for this. To bind to "MyMenuItems" in the view model, it's necessary to use a proxy as outlined in this answer. So instead of <CollectionContainer Collection="{Binding MyMenuItems}" />, you would end up using:
<CollectionContainer Collection="{Binding Path=DataContext.MyMenuItems,Source={StaticResource ProxyElement}}" />
And adding the proxy to the top of the view:
<UserControl.Resources>
<FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
<UserControl.Resources>
<ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>

Related

WPF shared menu between main top menu and right click context menu

I'm trying to make a shared menu between something on my top menu bar of my app, and the right click context menu of something in my interface in WPF. I've googled aroudn but I can't figure out hwo to share ONLY the menuitems list.
Here is a picture of the UI to help describe it:
The way this works is when an item in the list (as shown in the background) is selected, this menu becomes available to use. I would like to make it so that when you right click an item in the list, it also shows the same menu. I would like to avoid duplicating code, so I defined a resource for MenuItem in my window resources:
<MenuItem x:Key="modUtilsMenu">
<MenuItem Header="{Binding SelectedMod.ModName}" IsEnabled="False" FontWeight="Bold" />
<MenuItem Header="{DynamicResource string_Checkforupdates}" Command="{Binding SelectedModCheckForUpdatesCommand}" ToolTip="{DynamicResource string_tooltip_checksForUpdatesToThisMod}" >
<MenuItem.Icon>
<fa:ImageAwesome Style="{StaticResource EnableDisableImageStyle}" Icon="Cloud" Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Height="16" Width="16"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{DynamicResource string_RestoremodfromME3Tweaks}" Command="{Binding RestoreModFromME3TweaksCommand}" ToolTip="{DynamicResource string_tooltip_forcesUpdateCheck}" >
<MenuItem.Icon>
<fa:ImageAwesome Style="{StaticResource EnableDisableImageStyle}" Icon="CloudDownload" Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Height="16" Width="16" RenderOptions.BitmapScalingMode="HighQuality"/>
</MenuItem.Icon>
</MenuItem>
...
I then add it to the interface as a sub element of the Mod Utils menuitem:
<MenuItem Header="{DynamicResource string_ModUtils}" Padding="4" IsEnabled="{Binding SelectedMod, Converter={StaticResource NullEnabledConverter}}">
<StaticResource ResourceKey="modUtilsMenu"/>
</MenuItem>
Obviously this doesn't work as it has a second MenuItem defined in the resource.
However, I am not sure how I can store a "list" of menu items to add as children of another object, as the root container element of MenuItem and ContextMenu are not the same. These are all command based menu items. I will have the same issue with a context menu too - how do I only share the contents and not the container? Do I have to do data binding?
I have looked at How do I share a menu definition between a context menu and a regular menu in WPF, but that seems to be just for single menu items. I suppose I could do it for every one of them, but I'm looking to see if there's a way to do this where I only have to update it in one place instead of three to make it work.
Menu and ContextMenu are both of type ItemsControl. You can treat them like this e.g. bind to a collection of item models and specify a DataTemplate.
The following example creates a collection of MenuItem as XAML resource.
To allow multiple instances of the collection it is important to to set the x:Shared attribute to False. Otherwise the menu will be rendered only in one location of the visual tree, no matter the number of references:
<Window>
<Window.Resources>
<x:Array x:Key="SharedMenuItems"
Type="MenuItem"
x:Shared="False">
<MenuItem Header="File">
<MenuItem Header="Save" />
</MenuItem>
<MenuItem Header="Settings" />
</x:Array>
</Window.Resources>
<StackPanel x:Name="RootPanel" viewModels:Item.IsMarkedAsRead="True">
<Menu ItemsSource="{StaticResource SharedMenuItems}" />
<Grid>
<Grid.ContextMenu>
<ContextMenu ItemsSource="{StaticResource SharedMenuItems}" />
</Grid.ContextMenu>
</Grid>
</StackPanel>
</Window>

How to merge two sources in a Collection and show them correct in a combobox?

I have some XAML code that makes me mad. It started all with adding a dummy Item for a not referenced value.
For this I had to implement a CollectionViewSource and a CompositeCollection.
Now I can't select the first Combobox Item, it appears but I cannot select it, because I set the DisplayMemberPath in XAML (I guess so). Also the separator looks not as expected.
Let me show you:
If I don't set the XAML DisplayMemberPath, I can use the Dummy Item but the bound ones are displayed incorrect:
XAML:
<ComboBox x:Name="G_cb_content_zuordnung"
Margin="165,0,0,0"
Grid.Row="1"
SelectedIndex="0"
VerticalAlignment="Top"
DisplayMemberPath="PartnerID"
HorizontalAlignment="Left"
Width="119">
<ComboBox.Resources>
<CollectionViewSource x:Key="ComboCollection" Source="{Binding Path=mySelectedItem.Stammkinder}" />
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<ComboBoxItem Content="Ohne Stammnummer" Name="NoPID" />
<Separator />
<CollectionContainer Collection="{Binding Source={StaticResource ComboCollection}, Mode=OneWay}" />
</CompositeCollection>
</ComboBox.ItemsSource>
</ComboBox>
All I need is a dummy / placeholder combobox item that gets shown on top of the ObservableCollection<myClass>. Is my way of thinking wrong? Is there a smarter solution? Do I miss something in my solution?
Use your second approach and define a DataTemplate for the items explicitly, rather than using the DisplayMemberPath property:
<ComboBox xmlns:o="clr-namespace:APP.GLN_Organisator.Objects">
<ComboBox.Resources>
<CollectionViewSource x:Key="ComboCollection"
Source="{Binding Path=mySelectedItem.Stammkinder}" />
<!-- Define a DataTemplate here -->
<DataTemplate DataType="{x:Type o:ChildPartner}">
<TextBlock Text="{Binding PartnerID}"/>
</DataTemplate>
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<ComboBoxItem Content="Ohne Stammnummer" Name="NoPID" />
<Separator />
<CollectionContainer Collection="{Binding Source={StaticResource ComboCollection}}" />
</CompositeCollection>
</ComboBox.ItemsSource>
</ComboBox>
With a DataTemplate, you tell WPF how you want to display your items. If you don't provide any DataTemplate and don't set the DisplayMemberPath property value, WPF falls back to the simple ToString() call for displaying your items. That's why you see these type strings instead of your items.

Populating MenuItem with composite collection "doubling" up

I'm trying to populate a menu item with a collection of children items. That was easy, however I need to add an extra that's always present that performs an "add" operation. I used a composite collection to add this on to the existing collection of items that need to be displayed.
Here's the code:
<MenuItem Header="Time Ranges" x:Name="TimeRangesMenuItem"
Background="{StaticResource TitleBarButtonBackgroundBrush}"
Margin="2">
<MenuItem.Resources>
<CollectionViewSource Source="{Binding ElementName=TimeRangesMenuItem, Path=DataContext.TimeSpans}" x:Key="TimeSpanMenuItems" />
</MenuItem.Resources>
<MenuItem.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource TimeSpanMenuItems}}" />
<Separator />
<MenuItem Header="Add New" cal:Message.Attach="NewTimeSpan()" />
</CompositeCollection>
</MenuItem.ItemsSource>
<MenuItem.ItemTemplate>
<ItemContainerTemplate>
<MenuItem Header="{Binding Name}" cal:Message.Attach="ConfigureTimeSpan()" />
</ItemContainerTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
It works, almost. The DataContext.TimeSpans ViewModel's I'm trying to display are "nested" inside two MenuItems, rather than just populating one, so I get this strange behavior (this only has a single TimeSpanViewModel "Time Span":
Mouse outside the "inner" MenuItem. Note, the inner one is functional if you press it:
Normal MenuItem:
Anyone got any ideas on how to fix this?
What is an item ItemContainerTemplate...
I think you want to manipulate the MenuItem.ItemContainerStyle instead, otherwise you create two MenuItems as observed.
The MenuItem.ItemTemplate already defines what is inside the given item container (varies by control, here MenuItem, in a ComboBox it's a ComboBoxItem, etc.). As there does not appear to be an ItemContainerTemplate property on the MenuItem you might only be able to use it that way implementing a selector that returns your template and setting it as ItemContainerTemplateSelector.

Dynamically binding and statically adding MenuItems - using view Models/MVVM

I'm trying to have a dynamic menu item using MVVM from an observable collection. Everything worked, but then I needed to add a "add new" button to the end. I found a solution using a CompositeCollection, like here:
How do I dynamically bind and statically add MenuItems?
So have the following code, where TimeSpans is a collection of ViewModels:
<MenuItem Header="Time Ranges">
<MenuItem.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding TimeSpans}" />
<Separator />
<MenuItem Header="Add New" cal:Message.Attach="NewTimeSpan()" />
</CompositeCollection>
</MenuItem.ItemsSource>
<MenuItem.ItemTemplate>
<ItemContainerTemplate>
<MenuItem Header="{Binding Name}" cal:Message.Attach="ConfigureTimeSpan()" />
</ItemContainerTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
However, the view models are not populated like it was just using ItemsSource="{Binding TimeSpans}", it's not showing anything:
I suspect this is because I'm in the StackOverflow answer above the binding is actually a collection of MenuItems, so that composite collection makes sense. Whereas mine's mixing ViewModels & MenuItems.
Is there any way to construct the collection of menu-items created from ViewModels in the XAML so I can bind it?
For anyone else who comes across this, as Szabolcs Dezsi said, I needed to use a resource for the CollectionViewSource (bad reading comprehension on my part, as that was in the answer linked in my question).
Working code below:
<MenuItem Header="Time Ranges" x:Name="TimeRangesMenuItem">
<MenuItem.Resources>
<CollectionViewSource Source="{Binding ElementName=TimeRangesMenuItem, Path=TimeSpans}" x:Key="TimeSpanMenuItems" />
</MenuItem.Resources>
<MenuItem.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource TimeSpanMenuItems}}" />
<Separator />
<MenuItem Header="Add New" cal:Message.Attach="NewTimeSpan()" />
</CompositeCollection>
</MenuItem.ItemsSource>
<MenuItem.ItemTemplate>
<ItemContainerTemplate>
<MenuItem Header="{Binding Name}" cal:Message.Attach="ConfigureTimeSpan()" />
</ItemContainerTemplate>
</MenuItem.ItemTemplate>
</MenuItem>

How to Interact with 2 or More ViewModels

I have written a tool in which a ListBox is bound to a ObserservableCollection<object> with varying datatypes I've define. I use a PropertyDataTemplateSelector to present the data in the ListBox. The PropertyDataTemplateSelector references several DataTemplates that are set as UserControls. There is a background class that provides logic to the PropertyDataTemplateSelector by checking the object type and then applying the correct DataTemplate.
Here's an abbreviated example of the XAML for the UserControls and the MainWindow.
UserControl1
<TextBlock Text="{Binding Path=Val1}"
Style="{StaticResourse Yes}" />
<Button Content="I'm Button 1"
Command="{Binding Path=PathtoCommand1}"
CommandParameter="{Binding Parameter1}"
IsEnabled="{Binding IsEnabled1}" />
<Button Content="I'm Button 2"
Command="{Binding Path=PathtoCommand2}"
CommandParameter="{Binding Parameter2}"
IsEnabled="{Binding IsEnabled2}"
Tag="{Binding Path="DataContext.TagItem2}">
<Button.ContextMenu>
<ContextMenu>
<MenuItem IsCheckable="True"
IsChecked="{Binding Path=Tag}"
DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
</ContextMenu>
</Button.ContextMenu>
</Button>
</StackPanel>
</UserControl>
UserControlN
<UserControl x:Class="AwesomerControl">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=FancyName2}"
Style="{StaticResourse Yes}" />
<Button Content="Clicker 1"
Command="{Binding Path=DoSomethingGreat1}"
CommandParameter="{Binding Greatness1}"
IsEnabled="{Binding IsTurnedOn1}" />
<Button Content="Clicker 2"
Command="{Binding Path=DoSomethingGreat2}"
CommandParameter="{Binding Greaterness2}"
IsEnabled="{Binding IsTurnedOn2}"
Tag="{Binding Path="DataContext.TagItem2}">
<Button.ContextMenu>
<ContextMenu>
<MenuItem IsCheckable="True"
IsChecked="{Binding Path=Tag}"
DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
</ContextMenu>
</Button.ContextMenu>
</Button>
</StackPanel>
</UserControl>
Here I set the UserControls to a specified DataTemplate. The UserControls were moved out to make the XAML easier to read/navigate. In actuality the UserControls are a few hundred lines each.
<Window.Resources>
<DataTemplate x:Key"Template1">
<customControls:AwesomeControl/>
</DataTemplate>
...
<DataTemplate x:Key"TemplateN">
<customControls:AwesomerControl/>
</DataTemplate>
<dts:PropertyDataTemplateSelector x:Key="templateselector"
Template1="{StaticResource Template1"}
...
TemplateN="{StaticResource TemplateN"}
</Window.Resources>
The ListBox is defined as this.
<ListBox ItemSource="{Binding Path=CollectionofMyObjects}"
ItemTemplateSelector="{StaticResource templateselector}" />
I am using a single ViewModel to drive the MainWindow and the UserControls.
So that's where I'm at, essentially. I have this currently working as I'd like, but in an ongoing effort to learn (this is my first MVVM/WPF/C# project) I'd like to keep exploring how to make my code "better" (however that's defined). I'm not looking to solve an error here. So to avoid a general/broad question, I'll ask what I think I want to know. Someone can correct me and I'll update the "question(s)" appropriately
Question: How can I go about producing a ViewModel for each of the UserControls? Some of the ViewModels, for the UserControls, will occasionally require two-way communication to the MainWindow_ViewModel. The main crux of my problem is figuring out how the multiple VMs will communicate.
You're close, but it's not quite MVVM yet. ;)
First, break out all the functionality that is relevant to each UserControl into their own classes. These are your view-model classes.
Your controls should now become "view" classes, and they deserve their own mark-up file. Rather than use a template selector, you can use the DataTemplate.DataType to automatically connect the view-model class type to its view.
There are a lot of options for communication between view-models. To further your education, I'd consider looking at a light-weight MVVM framework that has built-in solutions for communication. My personal favorite is Caliburn.Micro, which includes an EventAggregator, a service that provides the ability to publish an object from one view-model to another in a loosely-coupled fashion.
Keep learning, you're on the right track!

Categories