WPF Ribbon Binding to ViewModel HierarchicalDataTemplate Strange behaviour - c#

My plan is to build a large WPF application containing multiple user controls as modules which are loaded at need. These modules provide a own list of menu items to be displayed in the main window.
The menu provided is a list of MenuTab objects.
public class MenuTab
{
private string _label;
private List<MenuGroup> _menuGroups = new List<MenuGroup>();
public string Label
{
get { return _label; }
set { _label = value; }
}
public List<MenuGroup> MenuGroups
{
get { return _menuGroups; }
}
public MenuTab(string label)
{
Label = label;
}
}
public class MenuGroup
{
private string _label;
private string _description;
private List<MenuEntry> _menuEntries = new List<MenuEntry>();
public string Label
{
get { return _label; }
set { _label = value; }
}
public string Description
{
get { return _description; }
set { _description = value; }
}
public List<MenuEntry> MenuEntries
{
get { return _menuEntries; }
}
public MenuGroup(string label)
{
Label = label;
}
}
public class MenuEntry
{
private string _label;
private BitmapSource _largeImage;
private BitmapSource _smallImage;
private ICommand _command;
public string Label
{
get { return _label; }
set { _label = value; }
}
public BitmapSource LargeImage
{
get { return _largeImage; }
set { _largeImage = value; }
}
public BitmapSource SmallImage
{
get { return _smallImage; }
set { _smallImage = value; }
}
public ICommand Command
{
get { return _command; }
set { _command = value; }
}
public MenuEntry(string label)
{
Label = label;
}
}
I found multiple hints in the internet how to build the menu to add additional static menus and so on.
Here how I build the menu and the HierarchicalDataTemplate which basically result in the correct number of tabs, groups and items.
Even command binding is working fine.
<RibbonWindow.Resources>
<Style x:Key="ModuleGroup" TargetType="RibbonGroup">
<Setter Property="Background" Value="AntiqueWhite" />
<Setter Property="Foreground" Value="Green" />
</Style>
<HierarchicalDataTemplate DataType="{x:Type CommonModel:MenuTab}" ItemsSource="{Binding Path=MenuGroups}">
<RibbonTab Header="{Binding Path=Label}" Background="Orange" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type CommonModel:MenuGroup}" ItemsSource="{Binding Path=MenuEntries}">
<RibbonGroup Header="{Binding Path=Label}" Style="{StaticResource ModuleGroup}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type CommonModel:MenuEntry}">
<RibbonButton Label="{Binding Path=Label}" LargeImageSource="{Binding Path=LargeImage}" SmallImageSource="{Binding Path=SmallImage}" Command="{Binding Path=Command}" />
</DataTemplate>
<CollectionViewSource x:Key="ModuleMenuTabs" Source="{Binding ModuleMenu}"/>
</RibbonWindow.Resources>
And then between my static tabs:
<CollectionContainer Collection="{Binding Source={StaticResource ModuleMenuTabs}}"/>
But the tab header and background is not shown as excepted. And also the background of the generated groups is not as excepted (as example I added some background to one of the static groups which works fine.)
Can anybody give me a hint how to get the ribbons generated correctly?

After there were now responses I did further research and tests by myself.
I found the solution working for me - maybe this helps other.
My resources section in the XAML looks know like that:
<RibbonWindow.Resources>
<DataTemplate x:Key="buttonTempl">
<RibbonButton Label="{Binding Path=Label}" LargeImageSource="{Binding Path=LargeImage}" SmallImageSource="{Binding Path=SmallImage}" Command="{Binding Path=Command}" />
</DataTemplate>
<Style TargetType="RibbonGroup" x:Key="groupStyle">
<Setter Property="Header" Value="{Binding Label}"/>
<Setter Property="ItemsSource" Value="{Binding MenuEntries}"/>
<Setter Property="ItemTemplate" Value="{StaticResource buttonTempl}"/>
</Style>
<Style TargetType="RibbonTab" x:Key="tabStyle">
<Setter Property="Header" Value="{Binding Label}"/>
<Setter Property="ItemsSource" Value="{Binding MenuGroups}"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource groupStyle}"/>
</Style>
<CollectionViewSource x:Key="StaticMenuSrc" Source="{Binding StaticMenu}"/>
<CollectionViewSource x:Key="ModuleMenuSrc" Source="{Binding ModuleMenu}"/>
And the ribbon is defined as:
<Ribbon DockPanel.Dock="Top" ItemContainerStyle="{StaticResource tabStyle}" >
<Ribbon.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource StaticMenuSrc}}" />
<CollectionContainer Collection="{Binding Source={StaticResource ModuleMenuSrc}}" />
</CompositeCollection>
</Ribbon.ItemsSource>
</Ribbon>
This way I can provide a static application menu and a module-specific menu using MVVM.

Related

UserControl Listbox with a Generic ObservableCollection that can be Modified by Buttons?

I need to be able to display lists of data in a ListBox with buttons that can move the items Up and Down and Remove items from the ListBoxes and reflect that in the data models.
SampleDesign: http://bigriverrubber.com/_uploads/sites/2/usercontrollistbox.jpg
I plan on having multiple ListBoxes just like this with the same functionality across several windows, so I thought I could make a UserControl with the ListBox and buttons I need inside it and have the buttons modify the data. That way I could just pass an ObservableCollection to the UserControl and I wouldn't have to recreate the buttons each time.
What I found out, however, is that I can't move the items if they are bound to an ObservableCollection, which they need to be for my purposes. From what I've read, I need to modify the collection instead.
But how do I do that from the UserControl? If the Type of the ObservableCollection needs to be variable so the ListBox can display many Types of lists, how can I possibly hope to target it to gain access to the Move and Remove methods in the ObservableCollection class?
I've tried taking the ItemsSource which was set to the ObservableCollection and converting it into an ObservableCollection< dynamic > but that didn't work.
I've tried Casting it as an ObservableCollection< T > and ObservableCollection< object > among others to no avail.
I've even tried restructuring my ViewModels under a GenericViewModel with a property of ObservableCollection< dynamic >, which failed and left my code in ruin so I had to return to a backup.
I've used an ItemsControl that changes the ListBox depending on which DataType it finds, but that would still mean I have to make separate button events anyway, so what's the point?
I would post some code, but seeing how nothing I've done has worked in the slightest I doubt that it will help any. At this point I don't even know if what I'm intending can be done at all.
If there are any suggestions on what code to post, feel free to ask.
EDIT: Here is a GenericViewModel. It doesn't work because I don't know what to set "Anything" to. EDIT: Added the UserControl
public class GenericViewModel : Observable
{
//-Fields
private ObservableCollection<Anything> _items;
private Anything _selectedItem;
//-Properties
public ObservableCollection<Anything> Items
{
get { return _items; }
set { Set(ref _items, nameof(Items), value); }
}
public Anything SelectedItem
{
get { return _selectedItem; }
set { Set(ref _selectedItem, nameof(SelectedItem), value); }
}
//-Constructors
public GenericViewModel()
{
if (Items == null) Items = new ObservableCollection<Anything>();
}
//-Logic
public void MoveUp()
{
if (Items == null) return;
Helper.MoveItemUp(Items, _items.IndexOf(_selectedItem));
}
public void MoveDown()
{
if (Items == null) return;
Helper.MoveItemDown(Items, _items.IndexOf(_selectedItem));
}
public void Remove()
{
if (Items == null) return;
Helper.RemoveItem(Items, _items.IndexOf(_selectedItem));
}
}
UserControl
public partial class CustomListBox : UserControl
{
//-Fields
//-Properties
//-Dependencies
//-Constructor
public CustomListBox()
{
InitializeComponent();
}
//-Methods
private void ListboxButtonUp_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonDown_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonCopy_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonDelete_Click(object sender, RoutedEventArgs e)
{
}
private void BorderLayerThumbnail_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
}
private void BorderLayerThumbnail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
}
}
<UserControl x:Class="BRRG_Scrubber.User_Controls.CustomListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BRRG_Scrubber"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="150">
<Grid Grid.Row="0" Margin="5,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}" Grid.Row="0" FontSize="10" Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
<!--ItemsSource="{Binding Items}" SelectedItem="{Binding Current}"-->
<ListBox x:Name="listBoxPlus" Grid.Row="1" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" >
<ListBox.Resources>
<Style x:Key="{x:Type ScrollBar}" TargetType="{x:Type ScrollBar}">
<Setter Property="Stylus.IsFlicksEnabled" Value="True" />
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Width" Value="14" />
<Setter Property="MinWidth" Value="14" />
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type local:Document}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Variable}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Layer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.30*" />
<ColumnDefinition Width="0.70*" />
</Grid.ColumnDefinitions>
<Border x:Name="borderLayerThumbnail" BorderBrush="#FF707070" BorderThickness="1" Width="50" Height="50" MouseRightButtonDown="BorderLayerThumbnail_MouseRightButtonDown" MouseLeftButtonDown="BorderLayerThumbnail_MouseLeftButtonDown" >
<Border.Background>
<ImageBrush ImageSource="/BRRG_Scrubber;component/Resources/Images/checkerboardtile.jpg" ViewportUnits="Absolute" Stretch="None" Viewport="0,0,12,12" TileMode="Tile"/>
</Border.Background>
<Image Grid.Column="0" Source="{Binding Image}" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" OpacityMask="Gray">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Visible}" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="10,0,0,0">
<TextBox Text="{Binding Name}"/>
<TextBlock Text="{Binding Type, Mode=OneWay}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Text="👁" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Visible}" Value="False">
<Setter Property="Opacity" Value="0.2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="🔒" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Locked}" Value="False">
<Setter Property="Opacity" Value="0.2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<WrapPanel Grid.Row="2" HorizontalAlignment="Right">
<WrapPanel.Resources>
<Style TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="20" />
</Style>
</WrapPanel.Resources>
<Button x:Name="listboxButtonUp" Content="▲" Click="ListboxButtonUp_Click"/>
<Button x:Name="listboxButtonDown" Content="▼" Click="ListboxButtonDown_Click"/>
<Button x:Name="listboxButtonCopy" Content="⧉" Click="ListboxButtonCopy_Click"/>
<Button x:Name="listboxButtonDelete" Content="⛞" Click="ListboxButtonDelete_Click"/>
</WrapPanel>
</Grid>
</UserControl>
I would really like to be able to create a modified ListBox in a UserControl with buttons that can move items Up and Down and Remove them from the list which I can use for any ObservableCollection of any unknown Type. The ListBoxes I need would all function exactly the same except their Type would be unknown until runtime.
EDIT: New Code From Ed's Suggestions
MainViewModel
public class MainViewModel : Observable
{
//-Fields
private Project _project;
private GenericViewModel<Document> _documentCollection;
private GenericViewModel<Variable> _variableCollection;
private GenericViewModel<Layer> _layerCollection;
//-Properties
public Project Project
{
get { return _project; }
set { Set(ref _project, nameof(Project), value); }
}
public GenericViewModel<Document> DocumentCollection
{
get { return _documentCollection; }
set { Set(ref _documentCollection, nameof(DocumentCollection), value); OnPropertyChanged(nameof(LayerCollection)); }
}
public GenericViewModel<Variable> VariableCollection
{
get { return _variableCollection; }
set { Set(ref _variableCollection, nameof(VariableCollection), value); }
}
public GenericViewModel<Layer> LayerCollection
{
get { return _layerCollection; }
set { Set(ref _layerCollection, nameof(LayerCollection), value); }
}
//-Constructors
public MainViewModel()
{
Project = new Project();
DocumentCollection = new GenericViewModel<Document>();
DocumentCollection.Items = Project.Documents;
}
//-Logic
}
Test Window with Bindings
<StackPanel>
<uc:CustomListBox DataContext="{Binding DocumentCollection}" Height="100"/>
<uc:CustomListBox DataContext="{Binding LayerCollection}" Height="200"/>
<ListBox ItemsSource="{Binding Project.Documents}" Height="100"/>
</StackPanel>
GenericViewModel
public class GenericViewModel<Anything> : Observable, ICollectionViewModel
{
//-Fields
private ObservableCollection<Anything> _items;
private Anything _selectedItem;
//-Properties
public ObservableCollection<Anything> Items
{
get { return _items; }
set { Set(ref _items, nameof(Items), value); }
}
public Anything SelectedItem
{
get { return _selectedItem; }
set { Set(ref _selectedItem, nameof(SelectedItem), value); }
}
//-Constructors
public GenericViewModel()
{
if (Items == null) Items = new ObservableCollection<Anything>();
}
//-Logic
...Removed For Brevity...
}
Document Model Class
public class Document : Anything
{
//-Fields
private string _filePath = "New Document";
private ObservableCollection<Layer> _layers;
private ObservableCollection<Selection> _selections;
//-Properties
public string FilePath
{
get { return _filePath; }
set { Set(ref _filePath, nameof(FilePath), value); }
}
public ObservableCollection<Layer> Layers
{
get { return _layers; }
set { Set(ref _layers, nameof(Layers), value); }
}
//-Constructors
public Document()
{
if (Layers == null) Layers = new ObservableCollection<Layer>();
if (Selections == null) Selections = new ObservableCollection<Selection>();
}
public Document(string filepath)
{
this.FilePath = filepath;
if (Layers == null) Layers = new ObservableCollection<Layer>();
if (Selections == null) Selections = new ObservableCollection<Selection>();
Layers.Add(new Layer("LayerOne "+Name));
Layers.Add(new Layer("LayerTwo " + Name));
Layers.Add(new Layer("LayerThree " + Name));
Selections.Add(new Selection());
Selections.Add(new Selection());
}
//-Gets
public string Name
{
get { return Path.GetFileNameWithoutExtension(FilePath); }
}
}
The big issue here seems to be that you can't cast to a generic class with an unknown type parameter, but you want your viewmodel class to be properly generic. That circle is squareable in two different useful ways, both valuable to know, so we'll do both.
The proper MVVM way to do this is to give your viewmodel some command properties which call these methods. A DelegateCommand class is the same as a RelayCommand class; the internet is full of implementations if you don't have one already.
public ICommand MoveUpCommand { get; } =
new DelegateCommand(() => MoveUp());
XAML:
<Button Content="▲" Command="{Binding MoveUpCommand}" />
Then get rid of those click event handlers. You don't need 'em. That very neatly solves your problem of calling those methods.
However, there's also a clean way to call those methods from code behind, and it's an important pattern to learn if you're going to be working with generics.
The classic solution to the casting problem is the one the framework uses for generic collections: Generic IEnumerable<T> implements non-generic System.Collections.IEnumerable. List<T> implements non-generic System.Collections.IList. Those non-generic interfaces provide the same operations, but in a non-generic way. You can always cast a List<T> to non-generic IList and call those methods and properties without knowing T.
Any well-designed collection can be assigned to a property of type IEnumerable: ListBox.ItemsSource is declared as System.Collections.IEnumerable, for example. Any collection can be assigned to it, without the ListBox needing to know what type is in the collection.
So let's write a non-generic interface that exposes the members we'll need to access without knowing any type parameters.
public interface ICollectionViewModel
{
void MoveUp();
void MoveDown();
void Remove();
}
If one of those method prototypes included the collection item type, say void RemoveItem(Anything x), that would complicate matters, but there's a classic solution to that problem as well.
Your Anything is already used like a type parameter. All we need to do is declare it as one. Your methods already have the appropriate prototypes to implement the interface methods.
public class GenericViewModel<Anything> : Observable, ICollectionViewModel
Instantiate like so:
this.DocumentCollection = new GenericViewModel<Document>();
Now your codebehind can cast any instance of GenericViewModel, regardless of type parameter, to a non-generic interface that supports the needed operations:
private void ListboxButtonUp_Click(object sender, RoutedEventArgs e)
{
if (DataContext is ICollectionViewModel icollvm)
{
icollvm.MoveUp();
}
}

How to manipulate specific ListViewItem from ListView

I want to mark a ListViewItem from a ListView by changing the foreground color of the textblock which is in the listviewitem. I want to do that dynamically. Whenever a user selects the item, the TextBlock's foreground color should be changed. Here is the Templete
<ListView Name="SongsListView"
IsItemClickEnabled="True"
ItemClick="SongsListView_ItemClick"
ItemsSource="{x:Bind rootpage.Songs}"
VerticalAlignment="Top"
HorizontalAlignment="Stretch">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Song">
<controls:DropShadowPanel ShadowOpacity="0.20"
Color="Black"
HorizontalContentAlignment="Stretch"
BlurRadius="10"
OffsetX="0"
OffsetY="7.0">
<Grid HorizontalAlignment="Stretch" CornerRadius="5" Background="{ThemeResource SystemControlAltHighAcrylicElementBrush}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind Name}" Name="songNameTextBlock" TextWrapping="Wrap"/>
<TextBlock Text="{x:Bind Artist}" Name="ArtistNameTextBlock" TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</controls:DropShadowPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="4"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
private void SongsListView_ItemClick(object sender, ItemClickEventArgs e)
{
//Please write the code for me !
}
I would suggest you to define a foreground relevant property in your Song class and bind it to the TextBlock's Foreground property. Then you could use Binding/{x:Bind} to change its foreground color when it's selected, instead of using the ItemClick event handler method to find the TextBlock controls in it.
Please refer to the following code sample for details:
<ListView Name="SongsListView"
IsItemClickEnabled="True"
ItemsSource="{x:Bind Songs}"
VerticalAlignment="Top"
HorizontalAlignment="Stretch" SelectedItem="{x:Bind currentSelectedItem,Mode=TwoWay,Converter={StaticResource myconverter}}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Song">
<controls:DropShadowPanel ShadowOpacity="0.20"
Color="Black"
HorizontalContentAlignment="Stretch"
BlurRadius="10"
OffsetX="0"
OffsetY="7.0">
<Grid HorizontalAlignment="Stretch" CornerRadius="5" Background="{ThemeResource SystemControlAltHighAcrylicElementBrush}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind Name}" Name="songNameTextBlock" TextWrapping="Wrap" Foreground="{x:Bind customcolor,Mode=OneWay}"/>
<TextBlock Text="{x:Bind Artist}" Name="ArtistNameTextBlock" TextWrapping="Wrap" Foreground="{x:Bind customcolor,Mode=OneWay}"/>
</StackPanel>
</Grid>
</controls:DropShadowPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="4"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
<Page.Resources>
<local:MyConverter x:Key="myconverter"></local:MyConverter>
</Page.Resources>
public sealed partial class MainPage : Page
{
public ObservableCollection<Song> Songs { get; set; }
private Song _currentSelectedItem;
public Song currentSelectedItem
{
get { return _currentSelectedItem; }
set
{
if (_currentSelectedItem != null)
{
_currentSelectedItem.customcolor = new SolidColorBrush(Colors.Black);
}
_currentSelectedItem = value;
_currentSelectedItem.customcolor = new SolidColorBrush(Colors.Red);
}
}
public MainPage()
{
this.InitializeComponent();
Songs = new ObservableCollection<Song>();
Songs.Add(new Song() {Name="abc",Artist="Singer1" });
Songs.Add(new Song {Name="def",Artist="Singer2" });
}
}
public class Song : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
private string _Name;
public string Name
{
get { return _Name; }
set
{
_Name = value;
RaisePropertyChanged("Name");
}
}
private string _Artist;
public string Artist
{
get { return _Artist; }
set
{
_Artist = value;
RaisePropertyChanged("Artist");
}
}
private SolidColorBrush _customcolor = new SolidColorBrush(Colors.Black);
public SolidColorBrush customcolor
{
get { return _customcolor; }
set
{
_customcolor = value;
RaisePropertyChanged("customcolor");
}
}
}
public class MyConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value as Song;
}
}
Please note that I make a converter class and use it when I bind currentSelectedItem to ListView's SelectedItem. Because I used x:Bind, it's compile time. If you do not add a converter, you will get error Invalid binding path 'currentSelectedItem' : Cannot bind type 'AppListView.model.Song' to 'System.Object' without a converter.
Besides, you could see I set Mode=OneWay when I use {x:Bind} to bind customcolor to TextBlock's foreground. It's due to the default value of {x:Bind}'s mode is OneTime. If you do not change OneTime to OneWay, you will not see the foreground color changed.

WPF command not working for submenu items in MVVM application

I have a menu which is built from a collection at runtime. This is all working as shown.
But if the menu contains child items (Child1, Child2 etc) the ReactiveCommand MenuCommand is never called.
If I remove all children from the menu so that the menu only contains parent items then MenuCommand is called. I am fairly new to WPF. I have re-created the problem in a sample app (code below). There are no visible binding errors in VS.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
public class Service
{
public Service(string menuHeading, string menuSubHeading)
{
MenuHeading = menuHeading;
MenuSubHeading = menuSubHeading;
}
public string MenuHeading { get; set; }
public string MenuSubHeading { get; set; }
}
public static class MenuBuilder
{
public static ReactiveList<MenuItem> Build(ReactiveList<Service> services)
{
ReactiveList<MenuItem> menuItems = new ReactiveList<MenuItem>();
foreach (var service in services)
{
AddOrUpdate(menuItems, service);
}
return menuItems;
}
private static void AddOrUpdate(ReactiveList<MenuItem> menu, Service service)
{
if (menu.Any((_ => _.Header.ToString() == service.MenuHeading)))
{
var item = menu.FirstOrDefault(x => x.Header.ToString() == service.MenuHeading);
item.Items.Add(new MenuItem() { Header = service.MenuSubHeading });
//if above line removed MenuCommand works
}
else
{
menu.Add(new MenuItem() { Header = service.MenuHeading });
var item = menu.FirstOrDefault(x => x.Header.ToString() == service.MenuHeading);
item.Items.Add(new MenuItem() { Header = service.MenuSubHeading });
//if above line removed MenuCommand works
}
}
}
public class MainWindowViewModel : ReactiveObject
{
public MainWindowViewModel()
{
MenuCommand = ReactiveCommand.Create<Object>(selectedItem => OnMenuItemSelected(selectedItem));
MenuCommand.Execute().Subscribe();
}
public ReactiveCommand<Object, Unit> MenuCommand { get; }
private ReactiveList<MenuItem> servicesMenu;
private ReactiveList<Service> Services = new ReactiveList<Service>()
{
new Service("Parent1", "Child1"),
new Service("Parent2", "Child1"),
new Service("Parent2", "Child2"),
};
public ReactiveList<MenuItem> ServicesMenu
{
get
{
if (servicesMenu == null)
{
servicesMenu = MenuBuilder.Build(Services);
return servicesMenu;
}
else
{
return servicesMenu;
}
}
}
private void OnMenuItemSelected(Object selectedItem)
{
//This method is only called when the menu does not contain any child items
}
}
<Grid>
<StackPanel Orientation="Vertical">
<Button Name="Button" Content="Button" Padding="5" HorizontalAlignment="Left"
Tag="{Binding RelativeSource={RelativeSource Self}, Path=DataContext}">
<Button.ContextMenu>
<ContextMenu x:Name="MainMenu" ItemsSource="{Binding ServicesMenu}"
DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command"
Value="{Binding DataContext.MenuCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}}" />
<Setter Property="CommandParameter"
Value="{Binding RelativeSource={RelativeSource Self}}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
</StackPanel>
</Grid>
Updated XAML after suggestions form Glenn
<Grid>
<StackPanel Orientation="Vertical">
<Button Name="Button" Content="Button" Padding="5" HorizontalAlignment="Left"
Tag="{Binding RelativeSource={RelativeSource Self}, Path=DataContext}">
<Button.ContextMenu>
<ContextMenu x:Name="MainMenu" ItemsSource="{Binding ServicesMenu}"
DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Header}" />
<Setter Property="Command" Value="{Binding Command}" />
<!--<Setter Property="Command" Value="{Binding MenuCommand}" /> was also tried-->
<Setter Property="CommandParameter" Value="{Binding}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
</StackPanel>
</Grid>
I suspect this is because child items placement target wouldn't be the Button like you expect, it would be the parent MenuItem.
One way I've gotten around this in the past is using MVVM approach for these type of menu items.
Create a Menu Item VM (you call them Service above) for your items (similar to what you already doing). In the VM have a Command property and pass in your command as part of it's constructor. Then you can just do {Binding MenuCommand} from your Item Container Style.
Also don't create the MenuItem's directly in your ViewModel, instead just bind direct to the Services. I would also recommend creating your sub-services as a ObservableCollection directly inside your Service, then in your item container set the ItemsSource property to bind to the sub-children of your Services.

wpf bind to selected listview item and update another list based on that selection

I'm using Visual Studio 2015, I'm trying to teach myself the MVVM pattern, and I'm hitting a road block. My code is loosely based off of Josh Smiths article, I'm using it to help me learn MVVM and create a small app for work in the process.
What I'm trying to accomplish:
I've bound a viewmodel to a listview showing a list of products, each product has a list of "productTemplate" items. In my View I would like this list to populate inside a listbox when a product from my list view is selected. I am implementing INotifyPropertyChanged. I think I'm just missing something simple but I'm not sure.
My code:
Two Models (Product, ProductTemplateItem);
public class Product {
private string _productNum;
private string _productFamily;
public Product() {
}
public string ProductNum { get; set; }
public string ProductFamily { get; set; }
}
public class ProductTemplateItem : ChangeEventHandlerBase {
private string _TemplateItem;
private string _TemplateCode;
public ProductTemplateItem(string templateItem, string templateCode) {
_TemplateItem = templateItem;
_TemplateCode = templateCode;
}
public string TemplateItem {
get { return _TemplateItem; }
set { if(_TemplateItem != value) {
_TemplateItem = value;
OnPropertyChanged("TemplateItem");
}
}
}
public string TemplateCode {
get { return _TemplateCode; }
set {
if (_TemplateCode != value) {
_TemplateCode = value;
OnPropertyChanged("TemplateCode");
}
}
}
public override string DisplayName {
get {
return $"{TemplateItem} - {TemplateCode}";
}
}
}
My ViewModels (Product View Model, brings everything together and adds the product template list, and AlProductsViewModel adds data and exposes everything to be bound in XAML):
public class ProductViewModel : BaseViewModel {
private Product _product;
private bool _isSelected;
private List<ProductTemplateItem> _productTemplate;
public ProductViewModel(string productNum, string productFamily) {
Product.ProductNum = productNum;
Product.ProductFamily = productFamily;
_productTemplate = new List<ProductTemplateItem>();
}
public string ProductNumber {
get { return _product.ProductNum; }
set { if(_product.ProductNum != value) {
_product.ProductNum = value;
OnPropertyChanged("ProductNumber");
}
}
}
public string ProductFamily {
get { return _product.ProductFamily; }
set {
if (_product.ProductFamily != value) {
_product.ProductFamily = value;
OnPropertyChanged("ProductFamily");
}
}
}
public bool IsSelected {
get { return _isSelected; }
set {
if (_isSelected != value) {
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
public List<ProductTemplateItem> ProductTemplate {
get { return _productTemplate; }
set { if (_productTemplate != value) {
_productTemplate = value;
OnPropertyChanged("ProductTemplate");
}
}
}
public Product Product {
get {
if (_product == null) {
_product = new Product();
return _product;
}
else {
return _product;
}
}
set { if(_product != value) {
_product = value;
OnPropertyChanged("Product");
}
}
}
public override string DisplayName {
get {
return Product.ProductNum;
}
}
}
public class AllProductsViewModel : BaseViewModel{
public AllProductsViewModel() {
AddProducts();
}
private void AddProducts() {
List<ProductViewModel> all = new List<ProductViewModel>();
all.Add(new ProductViewModel("4835", "Crop Cart"));
all.Add(new ProductViewModel("780", "Piler"));
all.Add(new ProductViewModel("880", "Piler"));
all.Add(new ProductViewModel("150", "Scooper"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Miscellaneous","MISC"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Drawbar", "DRBR"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Mainframe", "FRAM"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Conveyor", "CONV"));
all[1].ProductTemplate.Add(new Model.ProductTemplateItem("Hello", "HELL"));
AllProducts = new ObservableCollection<ProductViewModel>(all);
}
public ObservableCollection<ProductViewModel> AllProducts { get; private set; }
}
And my XAML code which is a user control with a ListView based off of Josh's code and a listbox that needs to be updated based off of the selection in the ListView:
<UserControl x:Class="Parts_Book_Tool.Views.ProductsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Parts_Book_Tool.Views"
xmlns:viewModel="clr-namespace:Parts_Book_Tool.ViewModel"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<viewModel:AllProductsViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
<CollectionViewSource x:Key="ProductGroups" Source="{Binding Path=AllProducts}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="ProductFamily"/>
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="ProductFamily" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<GroupStyle x:Key="ProductGroupStyle">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock
FontWeight="Bold"
Margin="1"
Padding="4,2,0,2"
Text="{Binding Path=Name}"
/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
<Style x:Key="MainHCCStyle" TargetType="{x:Type HeaderedContentControl}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<Border
Background="{StaticResource Brush_HeaderBackground}"
BorderBrush="LightGray"
BorderThickness="1"
CornerRadius="5"
Margin="4"
Padding="4"
SnapsToDevicePixels="True"
>
<TextBlock
FontSize="14"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
Text="{TemplateBinding Content}"
/>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ProductItemsStyle" TargetType="{x:Type ListViewItem}">
<!--
Stretch the content of each cell so that we can
right-align text in the Total Sales column.
-->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!--
Bind the IsSelected property of a ListViewItem to the
IsSelected property of a CustomerViewModel object.
-->
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ItemsControl.AlternationIndex" Value="1" />
<Condition Property="IsSelected" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#EEEEEEEE" />
</MultiTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<HeaderedContentControl Header="Model Info" Style="{StaticResource MainHCCStyle}" Grid.Row="0">
<ListView x:Name="lvModelNumbers" Margin="6,2,6,50" DataContext="{StaticResource ProductGroups}"
ItemContainerStyle="{StaticResource ProductItemsStyle}" ItemsSource="{Binding}" >
<ListView.GroupStyle>
<StaticResourceExtension ResourceKey="ProductGroupStyle"/>
</ListView.GroupStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="Model Number" Width="100" DisplayMemberBinding="{Binding Path=DisplayName}"/>
</GridView>
</ListView.View>
</ListView>
</HeaderedContentControl>
<HeaderedContentControl Header="Model Template" Style="{StaticResource MainHCCStyle}" Grid.Row="1">
<ListBox ItemsSource="{Binding SelectedItem/ProductTemplate, ElementName=lvModelNumbers}" Margin="6,2,6,50">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</HeaderedContentControl>
</Grid>
It feels to me like I'm missing the capture of an event to update the listbox, but with my inexperience in MVVM I Can't be sure. I've tried binding to the SelectedItem of the named element but that doesn't work. I can get the listbox to populate if I bind "AllProducts/ProductTemplate", but it just gives me the first indexed values, and doesn't dynamically change when I select another product.
Hopefully that is enough information, and any help would be greatly appreciated. I'm enjoying learning MVVM but it's dificult to wrap my head around.
Thanks,
Try bind the ListBox to the ProductTemplate property of the SelectedItem property of the ListView:
<ListView x:Name="lvModelNumbers" Margin="6,2,6,50"
ItemsSource="{Binding Source={StaticResource ProductGroups}}"
ItemContainerStyle="{StaticResource ProductItemsStyle}">
<ListView.GroupStyle>
<StaticResourceExtension ResourceKey="ProductGroupStyle"/>
</ListView.GroupStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="Model Number" Width="100" DisplayMemberBinding="{Binding Path=DisplayName}"/>
</GridView>
</ListView.View>
</ListView>
<ListBox ItemsSource="{Binding SelectedItem.ProductTemplate, ElementName=lvModelNumbers}" Margin="6,2,6,50">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

WPF: DataTrigger not firing

In a WPF Project I have some restyled DataGridColumnHeaders of a DataGrid which show a ComboBox for each DataGridColumnHeader. When the user selects from the ComboBox's the SelectionChanged handler (in the code behind) updates an Array of ColumnOptionViewModel objects on the MainWindowViewModel with the newest selection.
At this point some code also works out if there are any duplicate selections in this array, and then sets an IsDuplicate Boolean property on the ColumnOptionViewModel that are duplicates. The idea is that a DataTrigger picks up the change in IsDuplicate and changes the Background of a TextBlock in the DataTemplate of the ItemTemplate for the duplicate ComboBox's to Red.
However, this trigger is not firing. The IsDuplicate properties are being set ok, and everything else works as expected. What am I doing wrong?
Here is the XAML for the Window:
<Window x:Class="TestDataGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestDataGrid"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid Grid.Row="1" x:Name="dataGrid" ItemsSource="{Binding Records}">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<ComboBox x:Name="cbo"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.ColumnOptions}"
SelectionChanged="cbo_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="txt" Text="{Binding Name}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=cbo, Path=SelectedItem.IsDuplicate}">
<Setter TargetName="txt" Property="Background" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
</Grid>
CODE BEHIND:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel(RecordProvider.GetRecords());
}
private void cbo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var vm = (MainWindowViewModel)DataContext;
var selectionChangedCombo = (ComboBox)e.Source;
var dataGridColumnHeader = selectionChangedCombo.TemplatedParent as DataGridColumnHeader;
vm.ColumnSelections[dataGridColumnHeader.DisplayIndex] = selectionChangedCombo.SelectedItem as ColumnOptionViewModel;
CheckForDuplicates();
}
private void CheckForDuplicates()
{
var vm = (MainWindowViewModel)DataContext;
var duplicates = vm.ColumnSelections.GroupBy(x => x.Name)
.Where(g => g.Skip(1).Any())
.SelectMany(g => g);
foreach (var option in duplicates)
{
option.IsDuplicate = true;
}
}
}
MainWindowViewModel :
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<ColumnOptionViewModel> _columnOptions = new ObservableCollection<ColumnOptionViewModel>();
public ObservableCollection<RecordViewModel> _records = new ObservableCollection<RecordViewModel>();
ColumnOptionViewModel[] _columnSelections = new ColumnOptionViewModel[3];
public MainWindowViewModel(IEnumerable<Record> records)
{
foreach (var rec in records)
{
Records.Add(new RecordViewModel(rec));
}
ColumnOptions.Add(new ColumnOptionViewModel(TestDataGrid.ColumnOptions.ColumnOption1));
ColumnOptions.Add(new ColumnOptionViewModel(TestDataGrid.ColumnOptions.ColumnOption2));
ColumnOptions.Add(new ColumnOptionViewModel(TestDataGrid.ColumnOptions.ColumnOption3));
ColumnSelections[0] = ColumnOptions[0];
ColumnSelections[1] = ColumnOptions[1];
ColumnSelections[2] = ColumnOptions[2];
}
public ObservableCollection<ColumnOptionViewModel> ColumnOptions
{
get { return _columnOptions; }
set { _columnOptions = value; }
}
public ColumnOptionViewModel[] ColumnSelections
{
get { return _columnSelections; }
set { _columnSelections = value; }
}
public ObservableCollection<RecordViewModel> Records
{
get { return _records; }
set { _records = value; }
}
}
ColumnOptionViewModel :
public class ColumnOptionViewModel : ViewModelBase
{
ColumnOptions _colOption;
public ColumnOptionViewModel(ColumnOptions colOption )
{
_colOption = colOption;
}
public string Name
{
get { return _colOption.ToString(); }
}
public override string ToString()
{
return Name;
}
private bool _isDuplicate = false;
public bool IsDuplicate
{
get { return _isDuplicate; }
set
{ _isDuplicate = value;
OnPropertyChanged();
}
}
}
EDIT:
ViewModelBase :
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
If you are trying to bind to the IsDuplicate property of the SelectedItem in the ComboBox you could use a RelativeSource.
You should also set the Value property of the DataTrigger to true/false depending on when you want the Background property of the TextBlock to be set to Red:
<ComboBox x:Name="cbo" ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},Path=DataContext.ColumnOptions}"
SelectionChanged="cbo_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="txt" Text="{Binding Name}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=SelectedItem.IsDuplicate, RelativeSource={RelativeSource AncestorType=ComboBox}}" Value="True">
<Setter TargetName="txt" Property="Background" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
As #mm8 said, the Value is not chosen in your DataTrigger.
If that doesn't work, you can try using Trigger directly on TextBlock instead of DataTemplate.Triggers:
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Background" Value="White"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsDuplicate}" Value="True">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
Also, background and foreground values in controls that have selectable items can be tricky. For example, you may want to disable default selection colors (unfortunately then you will have to manage selected/focused background yourself):
<ComboBox.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
</ComboBox.Resources>

Categories

Resources