I just started with XAML/WPF and there are lots of questions going on in my head. One of them is how do we bind a button click to remove a ListBoxItem through the ICommand interface. I created a simple WPF project and here's my XAML:
<ListBox Name="lb" HorizontalAlignment="Left" Height="129" Margin="15,17,0,0" VerticalAlignment="Top" Width="314" Grid.ColumnSpan="2" >
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="Height" Value="30" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="5,5" Height="18" IsChecked="{TemplateBinding IsSelected}">
<ContentPresenter Content="{TemplateBinding Content}"/>
</CheckBox>
<Button Content="[x]" Height="22" Width="22" HorizontalAlignment="Right"
Command="{Binding ElementName=lb, Path=DataContext.DeleteItemCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}}" CommandParameter="{Binding }"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
<ListBoxItem Content="Foo" />
<ListBoxItem Content="Bar" />
</ListBox>
And here's my Window:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new Context(); // Also tried before InitializeComponent()
}
public class Context
{
public ICommand DeleteItemCommand = new DeleteItemCommand();
}
}
Where DeleteItemCommand is:
public class DeleteItemCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
MessageBox.Show("Meep");
}
}
The questions are:
Why isn't the message box showing? How do I make it work?
How do I retrieve which index/ListBoxItem triggered the button
click?
How do I align the button to the end of the line?
Thanks a lot!
One problem you have there is your ICommand is just a variable.
You need a public property in order to bind.
More like
public ICommand DeleteItemCommand {get;set;} = new DeleteItemCommand();
Another problem is your elementname. This is subject to namescope and I think you'll find the listbox is in another namescope.
Instead, just use relativesource binding with ancestortype ListBox.
Roughly.
Command="{Binding DataContext.DeleteItemCommand,
RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}
As an aside.
I recommend looking into a framework to make commands and suchlike easier.
MVVMLight would be my suggestion. Add to a project using nuget mvvmlightlibs. https://msdn.microsoft.com/en-gb/magazine/dn237302.aspx?f=255&MSPPError=-2147217396
The following is based on some code I already had so it's illustrative rather than exactly what you're doing.
View:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding People}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding LastName}"/>
<Button Content="Delete"
Command="{Binding DataContext.DeletePersonCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Viewmodel uses relaycommand from mvvmlight
using GalaSoft.MvvmLight.CommandWpf;
using System.Collections.ObjectModel;
namespace wpf_99
{
public class MainWindowViewModel : BaseViewModel
{
private RelayCommand<Person> deletePersonCommand;
public RelayCommand<Person> DeletePersonCommand
{
get
{
return deletePersonCommand
?? (deletePersonCommand = new RelayCommand<Person>(
(person) =>
{
People.Remove(person);
}
));
}
}
private ObservableCollection<Person> people = new ObservableCollection<Person>();
public ObservableCollection<Person> People
{
get { return people; }
set { people = value; }
}
public MainWindowViewModel()
{
People.Add(new Person { FirstName = "Chesney", LastName = "Brown" });
People.Add(new Person { FirstName = "Gary", LastName = "Windass" });
People.Add(new Person { FirstName = "Liz", LastName = "McDonald" });
People.Add(new Person { FirstName = "Carla", LastName = "Connor" });
}
}
}
BaseViewModel is pretty much as the msdn article on inotifypropertychanged shows:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Mvvmlight has its own base viewmodel but you can't serialise a vm inherits from that.
Person:
public class Person : BaseViewModel
{
private string firstName;
public string FirstName
{
get { return firstName; }
set { firstName = value; RaisePropertyChanged(); }
}
private string lastName;
public string LastName
{
get { return lastName; }
set { lastName = value; RaisePropertyChanged(); }
}
Related
Trying to get DataContext in UserControl.
My
structure
I have the model Car
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace AutoShop.MVVM.Model
{
class Car : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
private string _Model;
public string Model
{
get
{
return _Model;
}
set
{
_Model = value;
OnPropertyChanged();
}
}
private string _Mark;
public string Mark
{
get
{
return _Mark;
}
set
{
_Mark = value;
OnPropertyChanged();
}
}
private float _Volume;
public float Volume
{
get
{
return _Volume;
}
set
{
_Volume = value;
OnPropertyChanged();
}
}
private int _DateOfIssue;
public int DateOfIssue
{
get
{
return _DateOfIssue;
}
set
{
_DateOfIssue = value;
OnPropertyChanged();
}
}
public enum EngineTypes {
DISEL,
PETROL
};
private EngineTypes _EngineType;
public EngineTypes EngineType
{
get
{
return _EngineType;
}
set
{
_EngineType = value;
OnPropertyChanged();
}
}
private string _ImageUrl;
public string ImageUrl
{
get
{
return _ImageUrl;
}
set
{
_ImageUrl = value;
OnPropertyChanged();
}
}
public Car()
{
}
}
}
And I have main view model
using AutoShop.MVVM.Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace AutoShop.MVVM.ViewModel
{
class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
public HomeViewModel HomeVM;
private object _CurrentPage;
public object CurrentPage
{
get
{
return _CurrentPage;
}
set
{
_CurrentPage = value;
OnPropertyChanged();
}
}
private List<Car> _Cars;
public List<Car> Cars
{
get
{
return _Cars;
}
set
{
_Cars = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
Cars = new List<Car>() {
new Car
{
Mark = "Audi",
Model = "asdf",
Volume = 1.4F,
DateOfIssue = 2019,
EngineType = Car.EngineTypes.DISEL,
ImageUrl = "img/img"
},
new Car
{
Mark = "Moto",
Model = "asdf",
Volume = 1.4F,
DateOfIssue = 2019,
EngineType = Car.EngineTypes.DISEL,
ImageUrl = "img/img"
},
new Car
{
Mark = "Some",
Model = "asdf",
Volume = 1.4F,
DateOfIssue = 2019,
EngineType = Car.EngineTypes.DISEL,
ImageUrl = "img/img"
}
};
HomeVM = new HomeViewModel();
CurrentPage = HomeVM;
}
}
}
I need to display cars on the ProductPage.xaml and I do it by the next way
<UserControl x:Class="AutoShop.MVVM.View.ProductPage"
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:AutoShop.MVVM.View"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
>
<StackPanel Background="#fff">
<WrapPanel>
<Grid Width="200px" Margin="30 0 0 0">
<TextBox x:Name="field4" Tag="C:\Users\user\Desktop\Learning\Весенний семестр\ООТП\AutoShop\AutoShop\Images\u.png" Style="{StaticResource TextBoxTemplate}" />
<TextBlock IsHitTestVisible="False" Text="Марка" Padding="20 10 0 0">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="FontFamily" Value="/Fonts/#Montserrat Light" />
<Setter Property="Foreground" Value="#ccc" />
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=field4}" Value="">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Grid Width="200px" Margin="30 0 0 0">
<TextBox x:Name="field7" Tag="C:\Users\user\Desktop\Learning\Весенний семестр\ООТП\AutoShop\AutoShop\Images\u.png" Style="{StaticResource TextBoxTemplate}" />
<TextBlock IsHitTestVisible="False" Text="username" Padding="20 10 0 0">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="FontFamily" Value="/Fonts/#Montserrat Light" />
<Setter Property="Foreground" Value="#ccc" />
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=field7}" Value="">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Grid Width="200px" Margin="30 0 0 0">
<ComboBox Style="{StaticResource ComboBoxTheme}" SelectedIndex="0">
<ComboBoxItem>
<TextBlock Text="asdasdasd" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="fsdfsd" />
</ComboBoxItem>
</ComboBox>
</Grid>
</WrapPanel>
<Grid Height="400">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ItemsWrapper" ItemsSource="{Binding Cars}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="10" BorderBrush="Black" BorderThickness="2" Height="279">
<Grid Height="279" Width="200">
<Image Source="{Binding Path=Image}" Width="100" Height="100" VerticalAlignment="Top" Margin="0 10 0 0" />
<TextBlock Text="{Binding Path=Name, StringFormat='Name: {0}'}" VerticalAlignment="Top" Margin="0,120,0,0"/>
<TextBlock Text="{Binding Path=Mark, StringFormat='Rating: {0}' }" VerticalAlignment="Top" Margin="0 180 0 0" />
<TextBlock Text="{Binding Path=Model, StringFormat='Category: {0}'}" VerticalAlignment="Top" Margin="0,200,0,0" />
<TextBlock Text="{Binding Path=Volume, StringFormat='Price: {0}'}" VerticalAlignment="Top" Margin="0,160,0,0" />
<TextBlock Text="{Binding Path=DateOfIssue, StringFormat='Description: {0}'}" VerticalAlignment="Top" Margin="0,140,0,0" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</StackPanel>
</UserControl>
And it the MainForm.xaml I added ProductPage.xaml
<ContentControl Margin="10" Grid.Column="1" Content="{Binding CurrentPage}"/>
The problem is that nothing is being outputted, I think it might be due to the loss of context. How do I properly pass the context to UserControl (ProductPage.xaml)?
ProductPage.xaml
Update:
I set DataContext for MainWindow
And DataContext working becouse {Binding CurrentPage} is working, but binding on Cars field is not working
Add this to the Window.Resources. Of course DataContext must be set for the MainForm.
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type xmlnsForVewModel:MainViewModel}">
<ProductPage/>
</DataTemplate>
</Window.Resources>
</Window>
and if for ProductPage HomeViewModel supposed to be a DataContext, then HomeViewModel has to have Cars property, not MainViewModel.
1.find the MainForm.xaml.cs
2.find ctor
public MainForm()
{
InitializeComponent();
//set the MainForm DataContext
DataContext = new MainViewModel();
}
3.you should add ContentControl.DataTemplate to show your HomeViewModel
----20210427 update
Here demo i make, project name is WpfApp4
MainWindow.xaml
<Window x:Class="WpfApp4.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:WpfApp4"
xmlns:views="clr-namespace:WpfApp4.Views"
xmlns:viewModels="clr-namespace:WpfApp4.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<viewModels:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding ItemsSource, Mode=OneWay}" SelectedItem="{Binding SelectedCarViewModel, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Column="1">
<ContentControl Content="{Binding SelectedCarViewModel}">
<ContentControl.ContentTemplate>
<!--Style your product page here-->
<DataTemplate DataType="{x:Type viewModels:CarViewModel}">
<UniformGrid Columns="2">
<TextBlock Text="Name"/>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="Volume"/>
<TextBlock Text="{Binding Volume}"/>
<TextBlock Text="Price"/>
<TextBlock Text="{Binding Price}"/>
</UniformGrid>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace WpfApp4
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
MainWindowViewModel.cs
using System.Collections.ObjectModel;
namespace WpfApp4.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<CarViewModel> ItemsSource { get; } = new ObservableCollection<CarViewModel>();
private CarViewModel selectedCarViewModel;
public CarViewModel SelectedCarViewModel
{
get { return this.selectedCarViewModel; }
set { SetProperty(ref this.selectedCarViewModel, value); }
}
public MainWindowViewModel()
{
ItemsSource.Add(new CarViewModel() { Name = "BMW", Volume = 10, Price = 100 });
ItemsSource.Add(new CarViewModel() { Name = "Toyota", Volume = 5, Price = 80 });
ItemsSource.Add(new CarViewModel() { Name = "Benz", Volume = 20, Price = 150 });
// don't let view binding null value, so here initialze property "SelectedCarViewModel"
this.selectedCarViewModel = ItemsSource[0];
}
}
}
CarViewModel.cs
namespace WpfApp4.ViewModels
{
class CarViewModel : ViewModelBase
{
private string name;
public string Name
{
get { return this.name; }
set { SetProperty(ref this.name, value); }
}
private float volume;
public float Volume
{
get { return this.volume; }
set { SetProperty(ref this.volume, value); }
}
private decimal price;
public decimal Price
{
get { return this.price; }
set { SetProperty(ref this.price, value); }
}
}
}
ViewModelBase.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
namespace WpfApp4.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T t, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(t, value))
{
return false;
}
else
{
t = value;
RaisePropertyChanged(propertyName);
return true;
}
}
}
}
I have this object of class type HouseInfo that contains a list property:
public class HouseInfo
{
public string House
{
get;
set;
}
public List<String> Details
{
get;
set;
}
}
public List<HouseInfo> HouseInfos { get; set; }
I am successfully binding the House property to main items of combo box using ItemSource property in xaml but can't figure out the binding of Details to their respective submenus.
<ComboBox x:Name="Houses1"
Grid.Row="1"
Grid.Column="4"
ItemsSource="{Binding HouseInfos}"
Padding="0"
DisplayMemberPath="House"
VerticalContentAlignment="Center"
VerticalAlignment="Top"
HorizontalContentAlignment="Stretch"
Margin="0,0,0,2">
</ComboBox>
I tried customizing menuitems in xaml but I get the error "itemsCollection must be empty before using items Source."
How do I get the Details list in each menu item as submenu items?
Any help would be appreciated. Thanks in advance.
Update:
I have bound submenu items as well but they are not visible. I am sure they have bound successfully as it generates submenu items equal to the count of the list inside the details property list of the object. This is the updated xaml for the menu:
<Menu x:Name="menu"
VerticalAlignment="Top"
Grid.Row="1"
Grid.Column="4"
Height="19">
<MenuItem ItemsSource="{Binding HouseInfos}"
Padding="0"
Background="#0068FF11"
VerticalAlignment="Top"
RenderTransformOrigin="0.5,0.5"
Height="19"
Width="105">
<MenuItem.RenderTransform>
<TransformGroup>
<ScaleTransform />
<SkewTransform />
<RotateTransform />
<TranslateTransform X="0.5" />
</TransformGroup>
</MenuItem.RenderTransform>
<MenuItem.Header>
<Label x:Name="headerYears"
Margin="0"
Padding="0"
Content="Houses"
Background="#00FF0000"
MaxHeight="18"
UseLayoutRounding="False"
RenderTransformOrigin="0,0"
HorizontalContentAlignment="Center" />
</MenuItem.Header>
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header"
Value="{Binding House}" />
<Setter Property="ItemsSource"
Value="{Binding InfoPoints}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</Menu>
Here is the image of menu which is populated but not visible.
Bound but invisible submenu items
Try using the DataSource property of the combobox. You can assign HouseInfos.House1.
What I did was I dynamically assign them to the combobox
comboBox1.DataSource = HouseInfo.House1.Details;
comboBox1.DisplayMember = "HouseDetails";
comboBox1.ValueMember = "HouseDetailsID";
Or you can try something like the above.
Use this structure. I matched the names with your own names.
MainWindw.xaml
<Window x:Class="MyNameSpace.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:MyNameSpace"
mc:Ignorable="d"
Title="TestMenu" Height="450" Width="800">
<DockPanel>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding MenuItems}">
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</Menu.ItemContainerStyle>
<Menu.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:HouseInfo}" ItemsSource="{Binding Path=Details}">
<TextBlock Text="{Binding House}"/>
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
<Grid>
</Grid>
</DockPanel>
</Window>
MainWindow.cs
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
namespace MyNameSpace
{
/// <summary>
/// Interaction logic for MainWindw.xaml
/// </summary>
public partial class MainWindw : Window
{
public List<HouseInfo> MenuItems { get; set; }
public MainWindw()
{
InitializeComponent();
MenuItems = new List<HouseInfo>();
HouseInfo houseInfo1 = new HouseInfo();
houseInfo1.House = "Header A";
houseInfo1.Details = new List<HouseInfo>() { new HouseInfo() { House = "Header A1" }, new HouseInfo() { House = "Header A2" } };
HouseInfo houseInfo2 = new HouseInfo();
houseInfo2.House = "Header B";
houseInfo2.Details = new List<HouseInfo>() { new HouseInfo() { House = "Header B1" }, new HouseInfo() { House = "Header B2" } };
MenuItems.Add(houseInfo1);
MenuItems.Add(houseInfo2);
DataContext = this;
}
}
public class HouseInfo
{
public string House
{
get;
set;
}
public List<HouseInfo> Details { get; set; }
private readonly ICommand _command;
public HouseInfo()
{
_command = new CommandViewModel(Execute);
}
public ICommand Command
{
get
{
return _command;
}
}
private void Execute()
{
// (NOTE: In a view model, you normally should not use MessageBox.Show()).
MessageBox.Show("Clicked at " + House);
}
}
public class CommandViewModel : ICommand
{
private readonly Action _action;
public CommandViewModel(Action action)
{
_action = action;
}
public void Execute(object o)
{
_action();
}
public bool CanExecute(object o)
{
return true;
}
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
}
}
you can gave style to every element with this code
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</Menu.ItemContainerStyle>
for example add this line to HouseInfo class
public Thickness Margin { get; set; }
and MainWindow.cs
MenuItems = new List<HouseInfo>();
HouseInfo houseInfo1 = new HouseInfo();
houseInfo1.House = "Header A";
houseInfo1.Margin = new Thickness(5);
houseInfo1.Details = new List<HouseInfo>() { new HouseInfo() { House = "Header A1" }, new HouseInfo() { House = "Header A2", Margin=new Thickness(10) } };
and set Style in xaml
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
<Setter Property="Margin" Value="{Binding Margin}" />
</Style>
</Menu.ItemContainerStyle>
test:
When we have a custom listbox with a defined event-handling for the left-click of the mouse, and also an additional shape inside the ListBoxItem data template which needs to do some action when it is clicked how can we handle these
I have a Custom ListBox witch tries to handle the Click event :
In the ContentView:
<ABC:AListBox
ClickCommand="{Binding LaunchCommand}"
...>
</ABC:AListBox>
In it's datatemplate we have this:
<DataTemplate x:Key="ThisListTemplate">
<StackPanel ...>
<Border Grid.Column="1" VerticalAlignment="Center">
<TextBlock
FontSize="15"
Foreground="White"
Text="{Binding Path=ItemTitle}" />
</Border>
<Canvas Height ="12" Width ="12" >
<Ellipse Name = "TheEllipse" Stroke="Black" Height ="12"
Width ="12" Cursor="Hand" Canvas.Left="185" Canvas.Top="12">
</Ellipse>
<Ellipse.InputBindings>
<MouseBinding Gesture="LeftClick"
Command="{Binding DataContext.LaunchFromXamlCommand , RelativeSource={RelativeSource AncestorType=ABC:AListBox}}"
CommandParameter="{Binding}" />
</Ellipse.InputBindings>
</Canvas>
</StackPanel>
</DataTemplate>
And in the MVVM as our data context we have:
public ICommand LaunchCommand { get; private set; }
public DelegateCommand<object> LaunchFromXamlCommand { get; private set; }
// Initialization on load:
this.LaunchCommand = new DelegateCommand(this.LaunchRun);
this.LaunchFromXamlCommand = new DelegateCommand<object>(this.LaunchFromXamlRun);
//---------
private void LaunchFromXamlRun(object param)
{
TheListItem app = (TheListItem)param;
...
}
private void LaunchRun()
{ ... }
Here I used 2 different commands LaunchCommand as an ICommand in addition with LaunchFromXamlCommand which is called via the template.
The LaunchFromXamlRun will get triggered fine and as expected.
But also as it can be guessed there will be 2 raised events and 2 commands getting triggered which I want to omit one and ignore the general ListBox event handler when that shape is get hit.
What can be the best solution for doing this?
FYI: (Maybe not so important just for a note) The App is using earlier versions of Prism (don't think that matters here) and has a modular code, everything is separated in different assemblies and the code is using MVVM pattern.
I wish we had something like e.handled = true in a mechanism that could be used in the given scenario.
Your problem is exacerbated by having that click handler in your listbox. I don't know how you're doing that but that cannot just be click. It's probably previewmousedown. Because, of course, a listbox "eats" mousedown as part of selecting an item.
One way round this involves not using that listbox previewmousedown. Here I put my row content inside a button and bind the button's command. It doesn't look like a button of course.
I make the circle into a button and give it a transparent fill so you can click on all of it.
<ListBox ItemsSource="{Binding People}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.ItemClickCommand, RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}"
CommandParameter="{Binding}"
>
<Button.Template>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding LastName}"/>
<Button Command="{Binding DataContext.EllipseCommand, RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}"
>
<Button.Template>
<ControlTemplate>
<Ellipse Name = "TheEllipse" Stroke="Black"
Fill="Transparent"
Height ="12"
Width="12" Cursor="Hand">
</Ellipse>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</ControlTemplate>
</Button.Template>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
My viewmodel uses relaycommands but (obviously) any implementation of ICommand would do. I had People from the last question I did some work on.
public class MainWindowViewModel : BaseViewModel
{
private RelayCommand ellipseCommand;
public RelayCommand EllipseCommand
{
get
{
return ellipseCommand
?? (ellipseCommand = new RelayCommand(
() =>
{
Console.WriteLine("CIRCLE clicked");
}
));
}
}
private RelayCommand<Person> itemClickCommand;
public RelayCommand<Person> ItemClickCommand
{
get
{
return itemClickCommand
?? (itemClickCommand = new RelayCommand<Person>(
(person) =>
{
Console.WriteLine($"You clicked {person.LastName}");
person.IsSelected = true;
}
));
}
}
private ObservableCollection<Person> people = new ObservableCollection<Person>();
public ObservableCollection<Person> People
{
get { return people; }
set { people = value; }
}
public ListCollectionView LCV { get; set; }
public MainWindowViewModel()
{
People.Add(new Person { FirstName = "Chesney", LastName = "Brown" });
People.Add(new Person { FirstName = "Gary", LastName = "Windass" });
People.Add(new Person { FirstName = "Liz", LastName = "McDonald" });
People.Add(new Person { FirstName = "Carla", LastName = "Connor" });
}
}
When you click that outer button it will grab the click. Which is why I set IsSelected in the command so the item you click becomes selected via binding.
Have style for a series of buttons btn1, btn2, btn3, etc.
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
Now, I would like for the TextBlock name to be tied to the button name. For example - btn1's text block's name would be btn1Txt. The purpose of this would be the end user can assign each button its own text in a settings menu.
Any hints on how I would go about this? I admit I'm relatively new to WPF and bindings.
EDIT:::: WHAT I"VE GOT SO FAR THAT IS WORKING.
On load, the program checks the settings file for the Text for each button. Each button's content is assigned the proper information. Then inside the style, I bind the TextBlock Text to the content of the parent button.
This may not be the normal way of going about it, but it works
Method
List<string> MainButtons = Properties.Settings.Default.MainButtonNames.Cast<string>().ToList();
for (int i = 0; i < MainButtons.Count(); i++)
{
string actualNum = Convert.ToString((i + 1));
var MainButtonFinder = (Button)this.FindName("MainButton" + actualNum);
Console.WriteLine(MainButtonFinder.Name);
MainButtonFinder.Content = MainButtons[i];
Console.WriteLine(MainButtonFinder.Content);
}
Style
<Style x:Key="MainButtonStyle" TargetType="{x:Type Button}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Width" Value="100px"/>
<Setter Property="MinHeight" Value="50"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="20" Height="45" Width="100" Margin="0" Background="#FF99CCFF">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="LCARS" Foreground="White" Padding="5px" FontSize="18px" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>`
This is the wrong way to go about what you're trying to do. Here's the "right" way to do it. There's a fair amount of boilerplate code here, but you get used to it.
Write a button viewmodel and give your main viewmodel an ObservableCollection of those:
#region ViewModelBase Class
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
#endregion INotifyPropertyChanged
}
#endregion ViewModelBase Class
#region MainViewModel Class
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
ButtonItems.Add(new ButtonItemViewModel("First Command", "First Item", () => MessageBox.Show("First Item Executed")));
ButtonItems.Add(new ButtonItemViewModel("Second Command", "Second Item", () => MessageBox.Show("Second Item Executed")));
}
#region ButtonItems Property
public ObservableCollection<ButtonItemViewModel> ButtonItems { get; }
= new ObservableCollection<ButtonItemViewModel>();
#endregion ButtonItems Property
}
#endregion MainViewModel Class
#region ButtonItemViewModel Class
public class ButtonItemViewModel : ViewModelBase
{
public ButtonItemViewModel(String cmdName, String text, Action cmdAction)
{
CommandName = cmdName;
Text = text;
Command = new DelegateCommand(cmdAction);
}
#region Text Property
private String _text = default(String);
public String Text
{
get { return _text; }
set
{
if (value != _text)
{
_text = value;
OnPropertyChanged();
}
}
}
#endregion Text Property
#region CommandName Property
private String _commandName = default(String);
public String CommandName
{
get { return _commandName; }
private set
{
if (value != _commandName)
{
_commandName = value;
OnPropertyChanged();
}
}
}
#endregion CommandName Property
public ICommand Command { get; private set; }
}
#endregion ButtonItemViewModel Class
public class DelegateCommand : ICommand
{
public DelegateCommand(Action action)
{
_action = action;
}
private Action _action;
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action?.Invoke();
}
}
Make that MainViewModel the DataContext of your Window:
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
And here's how you can put it all together in the XAML:
<Grid>
<StackPanel Orientation="Vertical">
<GroupBox Header="Buttons">
<ItemsControl ItemsSource="{Binding ButtonItems}" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Margin="2"
MinWidth="80"
Content="{Binding Text}"
Command="{Binding Command}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
<GroupBox Header="Edit Buttons">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
Margin="2"
x:Name="ButtonEditorListBox"
ItemsSource="{Binding ButtonItems}"
HorizontalAlignment="Left"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2"
Text="{Binding CommandName}"
/>
<TextBlock
Margin="2"
Text="{Binding Text, StringFormat=': "{0}"'}"
/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
<ContentControl
Grid.Column="1"
Margin="8,2,2,2"
Content="{Binding SelectedItem, ElementName=ButtonEditorListBox}"
>
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock
Margin="2"
HorizontalAlignment="Stretch"
FontWeight="Bold"
Text="{Binding CommandName, StringFormat={}{0}: }"
/>
<TextBox
Margin="2"
HorizontalAlignment="Stretch"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
/>
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</GroupBox>
</StackPanel>
</Grid>
Back to your question:
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
You're doing styles wrong. Very, very wrong. I can help you fix it if you show me the style.
As I understand what you want is to modify the "text" of the button, it occurs to me that you can do it this way.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBox Text="{Binding ElementName=ButtonTest, Path=Content, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
<Button Name="ButtonTest" Grid.Row="1" Width="100" Height="40">
<Button.ContentTemplate>
<DataTemplate>
<TextBlock Foreground="Blue" Text="{Binding}"></TextBlock>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</Grid>
I'm trying to make Context Menu, which will have items depending on some data in code.
So, i have simple class, determining single item of menu
class ContextMenuItem
{
public string ItemHeader {get; set;}
public Command ItemAction {get; set;
}
where Command is implementation of ICommand, and stores action, which will be fired once this item is selected. Then i have class, serving as DataContext
class SomeClass
{
public List<ContextMenuItem> ContextMenuItems {get; set;}
public string SomeProperty {get; set;}
public string SomeAnotherProperty {get; set;}
}
So, ContextMenuItems is list of actions I need in my context menu, which can be generated using different approaches.
And I'm creating dynamic context menu, using this approach.
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
So, i was suspecting this to work well. But, for some reason, binding works not the way I want it to.
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
Somehow, data context for this lines is not ContextMenuItem, but SomeClass itself. So, i can bind SomeProperty and SomeAnotherProperty here, but not ItemHeader or ItemAction. And this ruins whole idea of dynamicaly created context menu.
So, how can i make this template recognize ContextMenuItem as its DataContext?
What i want to do can be accomplished using DataTemplate, but it gives us MenuItem inside MenuItem, and this is not good.
Update
Full xaml code involving ListBox
<ListBox Margin="5, 5" Background="White" ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="3,1">
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
<TextBlock Text="{Binding ObjectName}" Grid.Column="1" Margin="0,2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
There is a sneaky trick to making this work. Normally I'd just use a RelativeSource in the binding to have it tunnel up to something with a DataContext. The problem is that ContextMenu doesn't sit in the visual tree hierarchy, so RelativeSource has nothing to find.
The solution is outlined here:
http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited
Copy/paste this class into your project somewhere:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
Then reference the namespace of the BindingProxy at the top of your Window/UserControl/whatever:
xmlns:local="clr-namespace:INSERTYOURNAMESPACEHERE"
Add the BindingProxy as a resource to your ListBox:
<ListBox.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</ListBox.Resources>
And finally set the Source of your ContextMenu ItemsSource binding to the proxy:
<ContextMenu ItemsSource="{Binding Data.ContextMenuItems, Source={StaticResource proxy}}" >
Refer below code. it is working fine for me.
<Window x:Class="BindingListBox_Learning.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox Margin="5, 5" Background="White" ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="3,1">
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding ContextMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
<TextBlock Text="{Binding SomeProperty}" Grid.Column="1" Margin="0,2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
class MainViewModel
{
public List<SomeClass> SwitchAgents { get; set; }
public MainViewModel()
{
SwitchAgents = new List<SomeClass>();
SomeClass obj = new SomeClass();
obj.SomeProperty = "Test";
List<ContextMenuItem> lst = new List<ContextMenuItem>();
lst.Add(new ContextMenuItem() { ItemHeader = "Hi", ItemAction = new BaseCommand(MenuClick) });
obj.ContextMenuItems = lst;
SwitchAgents.Add(obj);
}
void MenuClick(object obj)
{
// Do Menu Click Stuff
}
}
class ContextMenuItem
{
public string ItemHeader { get; set; }
public ICommand ItemAction { get; set; }
}
class SomeClass
{
public List<ContextMenuItem> ContextMenuItems { get; set; }
public string SomeProperty { get; set; }
public string SomeAnotherProperty { get; set; }
}
public class BaseCommand : ICommand
{
private Predicate<object> _canExecute;
private Action<object> _method;
public event EventHandler CanExecuteChanged;
public BaseCommand(Action<object> method)
: this(method, null)
{
}
public BaseCommand(Action<object> method, Predicate<object> canExecute)
{
_method = method;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
{
return true;
}
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_method.Invoke(parameter);
}
}
Instead of BaseCommand you use RelayCommand from MVVMLight OR DelegateCommand from PRISM.