Binding dynamically created menu items to ICommand from Button.ContextMenu - c#

OK, a definite newbie here with WPF, and obviously need to keep learning more about MVVM, my code wasn't specifically designed that way, but I did designate one class to be the interface and controller for the GUI, whereas the model code resides in another set of classes. Have been scouring the web for examples, and questions similar to mine, of which there are plenty, but after three days of running through the maze I'm asking for help.
What I need is a simple dropdown menu, with items that can be dynamically updated (its an app that talks to a USB device, so however many are available should show up along with their device ID and serial number), and the currently selected item should show up on the Button (or whatever implementation of Dropdown menu I end up with). In this example, I just create a static list but that same list would be dynamically updated later on in the full app.
What I have so far looks like it is on the right track: I get the currently selected device id string to show up on the Button, and on pushing the Button, I get the list of all available devices (it doesn't bother me much that the currently selected device shows up redundantly in the list). However, I am not able to hook into any event when an item is selected, and thus can't update the item in the button, or do anything else for that matter.
My XAML below. Note that this was roughly hacked together, and there are some things in here that make no sense, like "IsActive" for the "IsChecked" property, that came from examples. The big problem is that as far as I can tell, none of the Setter properties in the ContextMenu.Resources seem to be doing anything at all...tried changing the fontsize to no avail. And the really big problem, of course, is that the "MyCommand" binding isn't working, that method never gets called.
<Label Content="Device Selected:" HorizontalAlignment="Left" Margin="25,22,0,0" VerticalAlignment="Top" Width="124" FontWeight="Bold" FontSize="14" Height="25"/>
<Button x:Name="DeviceSelMenuButton" Content="{Binding DeviceID_and_SN, Mode=TwoWay}" HorizontalAlignment="Left" Height="28" Margin="25,52,0,0" VerticalAlignment="Top" Width="187" FontSize="14" Click="DeviceSelMenuButton_Click">
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding DeviceID_SN_Collection, Mode=TwoWay}">
<ContextMenu.Resources>
<Style x:Key="SelectDeviceStyle" TargetType="MenuItem">
<Setter Property="Command" Value="{Binding MyCommand}"/>
<Setter Property="CommandTarget" Value="{Binding RelativeSource Self}"/>
<Setter Property="IsChecked" Value="{Binding IsActive}"/>
<Setter Property="IsCheckable" Value="True"/>
<Setter Property="FontSize" Value="14"/>
</Style>
</ContextMenu.Resources>
</ContextMenu>
</Button.ContextMenu>
</Button>
And the code from MainWindow.xaml.cs:
public partial class MainWindow : Window
{
CustomDeviceGUI _customDeviceGui = new CustomDeviceGUI();
public MainWindow()
{
InitializeComponent();
this.DataContext = _customDeviceGui;
}
private void DeviceSelMenuButton_Click(object sender, RoutedEventArgs e)
{
// " (sender as Button)" is PlacementTarget
(sender as Button).ContextMenu.IsEnabled = true;
(sender as Button).ContextMenu.PlacementTarget = (sender as Button);
(sender as Button).ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
(sender as Button).ContextMenu.IsOpen = true;
}
private void SomeMethod(object sender, DataTransferEventArgs e)
{
// TODO Somehow get the index of the selected menu item (collection index, 0-based)
// int selIndex = (sender as Button).ContextMenu.Items.IndexOf ??
_customDeviceGui.UpdateDeviceID("RelayPro id updated");
}
}
And the GUI code:
class CustomDeviceGUI : INotifyPropertyChanged
{
// Declare the event
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _deviceDisplayString;
private ICommand _updateMenu;
List<string> ControllerDeviceList = new List<string>();
private System.Collections.ObjectModel.ObservableCollection<string> _DeviceID_SN_Collection = new System.Collections.ObjectModel.ObservableCollection<string>();
// CTOR
public CustomDeviceGUI()
{
ControllerDeviceList.Add("CustomDevice Device 1");
ControllerDeviceList.Add("CustomDevice Device 2");
ControllerDeviceList.Add("CustomDevice Device 3");
ControllerDeviceList.Add("CustomDevice Device 6");
UpdateDeviceID(ControllerDeviceList[0]);
}
#region CustomDeviceGUI Properties
public System.Collections.ObjectModel.ObservableCollection<string> DeviceID_SN_Collection
{
get
{
_DeviceID_SN_Collection.Clear();
foreach (string str in ControllerDeviceList)
{
_DeviceID_SN_Collection.Add(str);
}
return _DeviceID_SN_Collection;
}
private set
{
_DeviceID_SN_Collection = value;
}
}
public string DeviceID_and_SN
{
get
{
return _deviceDisplayString;
}
private set
{
_deviceDisplayString = value;
}
}
public ICommand MyCommand
{
get
{
if (_updateMenu == null)
_updateMenu = new MyGuiCommand();
return _updateMenu;
}
}
#endregion
#region Public Methods
public void UpdateDeviceID(string deviceID)
{
this._deviceDisplayString = deviceID;
RaisePropertyChangeEvent("DeviceID_and_SN");
RaisePropertyChangeEvent("DeviceID_SN_Collection");
}
#endregion
protected void RaisePropertyChangeEvent(string name)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
try
{
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
catch (Exception e)
{
// ... TODO Remove this catchall or find specific exceptions
}
}
public class MyGuiCommand : ICommand
{
public void Execute(object parameter)
{
// Debug.WriteLine("Hello, world");
int hmm = 3;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged // was ;
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
} // class CustomDeviceGUI

All the changes I had to make were in XAML. Primarily it was a matter of using the ancestor to get the right data context. I also switched to ContextMenu.ItemContainer instead of ContextMenu.Resources.
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=DataContext.MyCommand}"/>
</Style>
</ContextMenu.ItemContainerStyle>

Eventough I'm not sure I think that the:
<Setter Property="Command" Value="{Binding MyCommand}"/>
binding needs a RoutedUICommand object.
EDIT:
Another thing that i have noticed is that you don't set any command bindings before. Like this:
<Window.CommandBindings>
<CommandBinding Command="MyCommand" Executed="Execute" />
</Window.CommandBindings>
just an example you can set CommandBindings to many others controls.

Related

C# WPF Directory Treeview with checkboxes: check items on building fails with empty PropertyChanged

In a WPF window I show a treeview with checkboxes with disks/directories on a Pc. When the user expands a node, an event calls folder_Expanded adding the subdirectories of that node.
What should happen is that certain directories show a color (this works) and certain directories are checked if they are found in a XML file. The user can then check or uncheck (sub)directories after which the modified directory selection is again stored in that xml file.
However, I can't get a checkbox in that treeviewitem checked with a certain directory. In the code of the expanded event, I test it with a sample directory. The background color works fine, but the IsSelected line is doing nothing. Reason is that PropertyChanged is null so it doesn't create an instance of PropertyChangedEventArgs. I would say I have everything: a model inheriting from INotifyPropertyChanged and assigned as DataContext in the XAML and setting the property IsChecked of the CheckBox as defined in the XAML via this model.
What do I miss?
Alternatively I would like to know if I can directly set the checkbox to checked, without databinding, like I set the background color? Problem with databinding is when it doesn't work there's no way to debug the code, it just doesn't work....
At the start:
SelectFilesModel selectFilesModel = new SelectFilesModel();
public SelectFiles()
{
InitializeComponent();
Window_Loaded();
}
void folder_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem item = (TreeViewItem)sender;
if (item.Items.Count == 1 && item.Items[0] == dummyNode)
{
item.Items.Clear();
try
{
foreach (string s in Directory.GetDirectories(item.Tag.ToString()))
{
TreeViewItem subitem = new TreeViewItem();
subitem.Header = s.Substring(s.LastIndexOf("\\") + 1);
subitem.Tag = s;
subitem.FontWeight = FontWeights.Normal;
subitem.Items.Add(dummyNode);
subitem.Expanded += new RoutedEventHandler(folder_Expanded);
if (s.ToLower() == "c:\\temp") // Sample directory to test
{
subitem.Background = Brushes.Yellow; // This works!
selectFilesModel.IsChecked = true; // Eventually PropertyChanged is always null!!
}
item.Items.Add(subitem);
}
}
catch (Exception e2)
{
MessageBox.Show(e2.Message + " " + e2.InnerException);
}
}
}
The XAML looks as follows:
<Window.DataContext>
<local:SelectFilesModel/>
</Window.DataContext>
<Grid>
<TreeView x:Name="foldersItem" SelectedItemChanged="foldersItem_SelectedItemChanged" Width="Auto" Background="#FFFFFFFF" BorderBrush="#FFFFFFFF" Foreground="#FFFFFFFF">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Name="img" Width="20" Height="20" Stretch="Fill"
Source="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=Header,
Converter={x:Static local:HeaderToImageConverter.Instance}}"
/>
<TextBlock Name="DirName" Text="{Binding}" Margin="5,0" />
<CheckBox Name="cb" Focusable="False" IsThreeState="True" IsChecked="{Binding IsChecked ,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center"/> </StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
and the model looks as follows:
public class SelectFilesModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
bool? _isChecked = false;
public bool? IsChecked
{
get { return _isChecked; }
set { this.SetIsChecked(value, true, true); }
}
void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
{
if (value == _isChecked)
return;
_isChecked = value;
RaisePropertyChanged("IsChecked");
}
void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); }
}
} // SelectFilesModel
It would be interesting to see how youuu initialize the TreeView. It really looks like the selectFilesModel is not source of any data binding. It's not even part of your tree.
You are adding TreeViewItem manually (which is not a good idea - see your problem, which wouldn't exist if you would focus on dealing with the data models instead). Because of adding TreeViewItem elements directly, the DataContext of the TreeViewItem is the item itself.
The DataContext of your HeaderTemplate is the header value, which in your case is a string. You see selectFilesModel is never involved.
CheckBox.IsChecked currently binds to this string and we all know string has no property IsChecked.
What you should do is to create the tree using SelectFilesModel.
The following example is your modified code. It is not tested and written with no editor so it may contain minor erros. It should be enough to show the pattern.
Also note that Directory.EnumerateDirectories will perform much better in your scenario than Directory.GetDirectories.
Create an enum to express different states. Each state will map to a color which you set in XAML using a trigger.
enum DirectoryState
{
Default = 0,
Special
}
Then modify SelectFilesModel to allow to reference its children (subdirectories) and add a State enum property
public class SelectFilesModel : INotifyPropertyChanged
{
// TODO::Implement constructor to initialize properties
public event PropertyChangedEventHandler PropertyChanged;
bool? _isChecked = false;
public bool? IsChecked
{
get { return _isChecked; }
set { this.SetValue(value, ref _isChecked, true, true); }
}
DirectoryState _state;
public DirectoryState State
{
get { return _state; }
set { this.SetValue(value, ref _state, true, true); }
}
string _path;
public string Path
{
get { return _path; }
set { this.SetValue(value, ref _path, true, true); }
}
public ObservableCollection<SelectFilesModel> Subdirectories { get; }
void SetValue<TValue>(TValue value, ref TValue field, bool updateChildren, bool updateParent, [CallerMemberName] string propertyName = null)
{
if (value == field)
return;
field = value;
RaisePropertyChanged(propertName);
}
void RaisePropertyChanged(string prop) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
Then build the tree using the model. Note that since Expanded is a routed event, you don't have to subscribe to each item explicitly. Just listen to the routed event.
ObservableCollection<SelectFilesModel> TreeRoot { get; }
public SelectFiles()
{
InitializeComponent();
Window_Loaded();
foldersItem.AddHandler(TreeViewItem.ExpandedEvent, new RoutedEventHandler(folder_Expanded)));
TreeRoot = new ObservableCollection<SelectFilesModel>() { new SelectFilesModel() };
foldersItem.ItemsSource = TreeRoot;
}
void folder_Expanded(object sender, RoutedEventArgs e)
{
var item = (sender as TreeViewItem).DataContext as SelectFilesModel;
if (item.Subdirectories.Count == 1 && item.Subdirectories[0] == dummyNode)
{
item.Subdirectories.Clear();
try
{
foreach (string s in Directory.EnumerateDirectories(item.Path))
{
var subitem = new SelectFilesModel() { Path = Path.GetDirectoryName(s) };
subitem.Subdirectories.Add(dummyNode);
if (subitem.Path.ToLower() == "c:\\temp") // Sample directory to test
{
subitem.State = DirectoryState.Special; // This works!
subitem.IsChecked = true; // This should work too
}
item.Subdirectories.Add(subitem);
}
}
catch (Exception e2)
{
MessageBox.Show(e2.Message + " " + e2.InnerException);
}
}
}
Finally define the data temnplate with the appropriate triggers and add it to e.g. TreeView.Resources:
<HierarchicalDataTemplate DataType="{x:Type SelectFilesModel}
ItemsSource="{Binding Subdirectories}">
<StackPanel Orientation="Horizontal">
<Image Name="img" Width="20" Height="20" Stretch="Fill"
Source="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=Header,
Converter={x:Static local:HeaderToImageConverter.Instance}}"
/>
<TextBlock Name="DirName" Text="{Binding Path}" Margin="5,0" />
<CheckBox Name="cb" Focusable="False" IsThreeState="True" IsChecked="{Binding IsChecked ,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center"/> </StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding State}" Value="{x:Static DirectoryState.Special}">
<Setter TargetName="DirName" Property="Foreground" Value="Yellow" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>

How to interact with UI elements when using MVVM/MVVMLight in a WPF app

Based on my code below, I want to be able to change the background color of a Button 2 when Button 1 is clicked.
XAML File
<Grid>
<Button x:Name="Button1"
Content="Button 1"
Command="{Binding Button1Command}"/>
<Button x:Name="Button2"
Content="Button 2"/>
</Grid>
ViewModel File
public class MyViewModel : ViewModelBase
{
public ICommand Button1Command{get;private set;}
public MyViewModel(){
Button1Command = new RelayCommand(() => button1_Click());
}
private void button1_Click()
{
Console.WriteLine("Button 1 clicked");
// how can I change the background color of Button 2 here
this.Dispatcher.Invoke(() => {
Button2.Background = Brushes.Red;
});
}
}
In addition to what pm_2 mentioned, you could take advantage of MVVMLight's Messenger class. The VM can send a message that is received by the View to change the background.
public class ChangeBackgroundMessage
{
public Brush TheColor { get; set; }
}
And then in your VM:
Button1Command = new RelayCommand(() => ExecuteButtonCommand());
....
private void ExecuteButtonCommand()
{
Messenger.Default.Send<ChangeBackgroundMessage>(new ChangeBackgroundMessage { TheColor = Brushes.Red } );
}
and in your View:
public partial class MyView : UserControl
{
public MyView()
{
InitializeComponent();
Messenger.Default.Register<ChangeBackgroundMessage>(this, m => ReceiveChangeBackgroundMessage(m);
}
private void ReceiveChangeBackgroundMessage(ChangeBackgroundMessage m)
{
// If you need to ensure this executes only on UI thread, use the
// DispatcherHelper class
DispatcherHelper.CheckBeginInvokeOnUI(() => button2.Background = m.TheColor);
}
}
Yet another alternative would be to have a "view service" that the View registers with it's ViewModel. For example:
public interface IMySpecificViewService
{
void ChangeButtonColor(Brush color);
}
In VM:
public IMySpecificViewService ViewService { get; set; }
and in View
public partial class MyView : UserControl, IMySpecificViewService
...
public MyView()
{
var vm = (MyViewModel)this.DataContext;
vm.ViewService = (IMySpecificViewService)this;
}
public void ChangeButtonColor(Brush color)
{
Button2.Background = color;
}
which can be called in your VM's command handler:
private void ExecuteButtonCommand()
{
ViewService?.ChangeButtonColor(Brushes.Red);
}
I find I use these approaches when I can't directly bind to a property in the VM, (or I don't want to bleed any View specific stuff in the VM) and I need more fine grained control over manipulating the controls.
There are two approaches to this that spring to mind - the first is to simply bind the background colour of Button2 to a property on the viewmodel. You could expose this from the view model as a brush; although the way that is more consistent with MVVM would be to create a value converter.
The idea being that the background of Button2, despite being linked to Button1, is actually linked to a state that has changed when Button1 is pressed; the value converter then maps the state (which is the domain of the ViewModel) with the colour (the domain of the view).
Doing is this way, means that you can change the state in the view model command of button1, but not have to involve the button1_click event, as it is now unnecessary.
This question illustrates how you might achieve this.
First of all you need to declare a property in your view model that will control the background color as well as a command handler which a button can call to toggle it. This might seem a little verbose but you soon get used to that with MVVM, and there are frameworks you can use to minimize that if it really bothers you. So here's the main view model:
public class MainViewModel : ViewModelBase
{
#region Background Color Flag
private bool _Flag;
public bool Flag
{
get { return this._Flag; }
set
{
if (this._Flag != value)
{
this._Flag = value;
RaisePropertyChanged(() => this.Flag);
}
}
}
#endregion Background Color Flag
#region Button Command Handler
private ICommand _ButtonCommand;
public ICommand ButtonCommand
{
get { return this._ButtonCommand = (this._ButtonCommand ?? new RelayCommand(OnButtonPressed)); }
}
private void OnButtonPressed()
{
this.Flag = !this.Flag;
}
#endregion Button Command Handler
public MainViewModel()
{
}
}
One of the objectives of MVVM is to have as loose coupling between the view and the view model as possible. The Button's command binding should be fairly straightforward, but to set the background of the second button you can use DataTriggers:
<StackPanel Orientation="Vertical">
<Button Content="Toggle Background" HorizontalAlignment="Left" VerticalAlignment="Top"
Command="{Binding ButtonCommand}" />
<Button Content="Hello World!" HorizontalAlignment="Left" VerticalAlignment="Top">
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Flag}" Value="False">
<Setter Property="Background" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Flag}" Value="True">
<Setter Property="Background" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
This will cause the second button's background to toggle between red and green as you click the first button:

WPF&MVVM: access to Controls from RelayCommand()

I need both operating by mouse clicking and operating by hotkeys in my WPF application. User's actions affects on both data and appearance of application controls.
For example, the following app will send data to tea machine. You can select the tea brand, type (hot or cold) and optional ingredients: milk, lemon and syrup.
Not good from the point of view of UI design, but just example:
If to click the dropdown menu or input Ctrl+B, the list of select options will appear.
If to click the "Hot" button on input Ctrl+T, button becomes blue and text becomes "Cold". If to click or input Ctrl+T again, button becomes orange and text becomes to "Hot" again.
If to click optional ingredient button or input respective shortcut, button's background and text becomes gray (it means "unselected"). Same action will return the respective button to active state.
If don't use MVVM and don't define shortcuts, the logic will be relatively simple:
Tea tea = new Tea(); // Assume that default settings avalible
private void ToggleTeaType(object sender, EventArgs e){
// Change Data
if(tea.getType().Equals("Hot")){
tea.setType("Cold");
}
else{
tea.setType("Hot");
}
// Change Button Appearence
ChangeTeaTypeButtonAppearence(sender, e);
}
private void ChangeTeaTypeButtonAppearence(object sender, EventArgs e){
Button clickedButton = sender as Button;
Style hotTeaButtonStyle = this.FindResource("TeaTypeButtonHot") as Style;
Style coldTeaButtonStyle = this.FindResource("TeaTypeButtonCold") as Style;
if (clickedButton.Tag.Equals("Hot")) {
clickedButton.Style = coldTeaButtonStyle; // includes Tag declaration
clickedButton.Content = "Cold";
}
else (clickedButton.Tag.Equals("Cold")) {
clickedButton.Style = hotTeaButtonStyle; // includes Tag declaration
clickedButton.Content = "Hot";
}
}
// similarly for ingredients toggles
XAML:
<Button Content="Hot"
Tag="Hot"
Click="ToggleTeaType"
Style="{StaticResource TeaTypeButtonHot}"/>
<Button Content="Milk"
Tag="True"
Click="ToggleMilk"
Style="{StaticResource IngredientButtonTrue}"/>
<Button Content="Lemon"
Tag="True"
Click="ToggleLemon"
Style="{StaticResource IngredientButtonTrue}"/>
<Button Content="Syrup"
Tag="True"
Click="ToggleSyrup"
Style="{StaticResource IngredientButtonTrue}"/>
I changed my similar WPF project to MVVM because thanks to commands it's simple to assign the shortcuts:
<Window.InputBindings>
<KeyBinding Gesture="Ctrl+T" Command="{Binding ToggleTeaType}" />
</Window.InputBindings>
However, now it's a problem how to set the control's appearance. The following code is invalid:
private RelayCommand toggleTeaType;
public RelayCommand ToggleTeaType {
// change data by MVVM methods...
// change appearence:
ChangeTeaTypeButtonAppearence(object sender, EventArgs e);
}
I need the Relay Commands because I can bind it to both buttons and shortcuts, but how I can access to View controls from RelayCommand?
You should keep the viewmodel clean of view specific behavior. The viewmodel should just provide an interface for all relevant settings, it could look similar to the following (BaseViewModel would contain some helper methods to implement INotifyPropertyChanged etc.):
public class TeaConfigurationViewModel : BaseViewModel
{
public TeaConfigurationViewModel()
{
_TeaNames = new string[]
{
"Lipton",
"Generic",
"Misc",
};
}
private IEnumerable<string> _TeaNames;
public IEnumerable<string> TeaNames
{
get { return _TeaNames; }
}
private string _SelectedTea;
public string SelectedTea
{
get { return _SelectedTea; }
set { SetProperty(ref _SelectedTea, value); }
}
private bool _IsHotTea;
public bool IsHotTea
{
get { return _IsHotTea; }
set { SetProperty(ref _IsHotTea, value); }
}
private bool _WithMilk;
public bool WithMilk
{
get { return _WithMilk; }
set { SetProperty(ref _WithMilk, value); }
}
private bool _WithLemon;
public bool WithLemon
{
get { return _WithLemon; }
set { SetProperty(ref _WithLemon, value); }
}
private bool _WithSyrup;
public bool WithSyrup
{
get { return _WithSyrup; }
set { SetProperty(ref _WithSyrup, value); }
}
}
As you see, there is a property for each setting, but the viewmodel doesn't care about how the property is assigned.
So lets build some UI. For the following example, generally suppose xmlns:local points to your project namespace.
I suggest utilizing a customized ToggleButton for your purpose:
public class MyToggleButton : ToggleButton
{
static MyToggleButton()
{
MyToggleButton.DefaultStyleKeyProperty.OverrideMetadata(typeof(MyToggleButton), new FrameworkPropertyMetadata(typeof(MyToggleButton)));
}
public Brush ToggledBackground
{
get { return (Brush)GetValue(ToggledBackgroundProperty); }
set { SetValue(ToggledBackgroundProperty, value); }
}
// Using a DependencyProperty as the backing store for ToggledBackground. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ToggledBackgroundProperty =
DependencyProperty.Register("ToggledBackground", typeof(Brush), typeof(MyToggleButton), new FrameworkPropertyMetadata());
}
And in Themes/Generic.xaml:
<Style TargetType="{x:Type local:MyToggleButton}" BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyToggleButton}">
<Border x:Name="border1" BorderBrush="Gray" BorderThickness="1" Background="{TemplateBinding Background}" Padding="5">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="border1" Property="Background" Value="{Binding ToggledBackground,RelativeSource={RelativeSource TemplatedParent}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Now, build the actual window content using this toggle button. This is just a rough sketch of your desired UI, containing only the functional controls without labels and explanation:
<Grid x:Name="grid1">
<StackPanel>
<StackPanel Orientation="Horizontal">
<ComboBox
x:Name="cb1"
VerticalAlignment="Center"
IsEditable="True"
Margin="20"
MinWidth="200"
ItemsSource="{Binding TeaNames}"
SelectedItem="{Binding SelectedTea}">
</ComboBox>
<local:MyToggleButton
x:Name="hotToggle"
IsChecked="{Binding IsHotTea}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="AliceBlue" ToggledBackground="Orange">
<local:MyToggleButton.Style>
<Style TargetType="{x:Type local:MyToggleButton}">
<Setter Property="Content" Value="Cold"/>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="Hot"/>
</Trigger>
</Style.Triggers>
</Style>
</local:MyToggleButton.Style>
</local:MyToggleButton>
</StackPanel>
<StackPanel Orientation="Horizontal">
<local:MyToggleButton
x:Name="milkToggle"
Content="Milk"
IsChecked="{Binding WithMilk}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="WhiteSmoke" ToggledBackground="LightGreen"/>
<local:MyToggleButton
x:Name="lemonToggle"
Content="Lemon"
IsChecked="{Binding WithLemon}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="WhiteSmoke" ToggledBackground="LightGreen"/>
<local:MyToggleButton
x:Name="syrupToggle"
Content="Syrup"
IsChecked="{Binding WithSyrup}"
VerticalAlignment="Center"
Margin="20" MinWidth="60"
Background="WhiteSmoke" ToggledBackground="LightGreen"/>
</StackPanel>
</StackPanel>
</Grid>
Notice the style trigger to change the button content between Hot and Cold.
Initialize the datacontext somewhere (eg. in the window constructor)
public MainWindow()
{
InitializeComponent();
grid1.DataContext = new TeaConfigurationViewModel();
}
At this point, you have a fully functional UI, it will work with the default mouse and keyboard input methods, but it won't yet support your shortcut keys.
So lets add the keyboard shortcuts without destroying the already-working UI. One approach is, to create and use some custom commands:
public static class AutomationCommands
{
public static RoutedCommand OpenList = new RoutedCommand("OpenList", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.B, ModifierKeys.Control)
});
public static RoutedCommand ToggleHot = new RoutedCommand("ToggleHot", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.T, ModifierKeys.Control)
});
public static RoutedCommand ToggleMilk = new RoutedCommand("ToggleMilk", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.M, ModifierKeys.Control)
});
public static RoutedCommand ToggleLemon = new RoutedCommand("ToggleLemon", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.L, ModifierKeys.Control)
});
public static RoutedCommand ToggleSyrup = new RoutedCommand("ToggleSyrup", typeof(AutomationCommands), new InputGestureCollection()
{
new KeyGesture(Key.S, ModifierKeys.Control)
});
}
You can then bind those commands to appropriate actions in your main window:
<Window.CommandBindings>
<CommandBinding Command="local:AutomationCommands.OpenList" Executed="OpenList_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleHot" Executed="ToggleHot_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleMilk" Executed="ToggleMilk_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleLemon" Executed="ToggleLemon_Executed"/>
<CommandBinding Command="local:AutomationCommands.ToggleSyrup" Executed="ToggleSyrup_Executed"/>
</Window.CommandBindings>
and implement the appropriate handler method for each shortcut in the window code behind:
private void OpenList_Executed(object sender, ExecutedRoutedEventArgs e)
{
FocusManager.SetFocusedElement(cb1, cb1);
cb1.IsDropDownOpen = true;
}
private void ToggleHot_Executed(object sender, ExecutedRoutedEventArgs e)
{
hotToggle.IsChecked = !hotToggle.IsChecked;
}
private void ToggleMilk_Executed(object sender, ExecutedRoutedEventArgs e)
{
milkToggle.IsChecked = !milkToggle.IsChecked;
}
private void ToggleLemon_Executed(object sender, ExecutedRoutedEventArgs e)
{
lemonToggle.IsChecked = !lemonToggle.IsChecked;
}
private void ToggleSyrup_Executed(object sender, ExecutedRoutedEventArgs e)
{
syrupToggle.IsChecked = !syrupToggle.IsChecked;
}
Again, remember this whole input binding thing is purely UI related, it is just an alternative way to change the displayed properties and the changes will be transferred to the viewmodel with the same binding as if the user clicks the button by mouse. There is no reason to carry such things into the viewmodel.
how I can access to View controls from RelayCommand?
You shouldn't. The whole point of MVVM (arguably) is to separate concerns. The 'state' that the ViewModel contains is rendered by the View (controls). The ViewModel/logic should never directly adjust the view - as this breaks the separation of concerns and closely couples the logic to the rendering.
What you need is for the view to render how it wants to display the state in the View Model.
Typically, this is done by bindings. As example: Rather than the ViewModel grabbing a text box reference and setting the string: myTextBox.SetText("some value"), we have the view bind to the property MyText in the view model.
It's the view's responsibility to decide how to show things on the screen.
That's all well and good, but how? I suggest, if you want to do this change using styles like you describe, I'd try using a converter that converts the using a binding to ViewModel state (Say, an enum property Hot or Cold):
<Button Content="Hot"
Tag="Hot"
Click="ToggleTeaType"
Style="{Binding TeaType, Converter={StaticResource TeaTypeButtonStyleConverter}}"/>
Note, we're using WPF's bindings. The only reference we've got tot he view model is through it's property TeaType.
Defined in your static resources, we have the converter:
<ResourceDictionary>
<Style x:Key="HotTeaStyle"/>
<Style x:Key="ColdTeaStyle"/>
<local:TeaTypeButtonStyleConverter
x:Key="TeaTypeButtonStyleConverter"
HotStateStyle="{StaticResource HotTeaStyle}"
ColdStateStyle="{StaticResource ColdTeaStyle}"/>
</ResourceDictionary>
And have the logic for converting from the TeaType enum to a Style in this:
public enum TeaType
{
Hot, Cold
}
class TeaTypeButtonStyleConverter : IValueConverter
{
public Style HotStateStyle { get; set; }
public Style ColdStateStyle { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
TeaType teaType = (TeaType)value;
if (teaType == TeaType.Hot)
{
return HotStateStyle;
}
else if (teaType == TeaType.Cold)
{
return ColdStateStyle;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
It could be made more generic and re-usable.
You should also take a look at toggle buttons, they deal with this kind of thing internally.

Correct binding for dropdown menu from ObservableCollection and get its index

Variations of this question seems to be quite common, but I have yet to find a solution that works for me. I am attempting to place a Dropdown menu within a Button.ContextMenu, using an Observable collection, and thought I was on the right track, with one piece missing: I have yet to be able to get the index of the item selected, and although I can see my collection in the debugger, I am beginning to wonder if the items are really getting found, will explain as I go. First, the XAML...you can see that I have a binding for the Button Content, and the idea is that after a menu item gets selected, my code behind will update that property. Which it could, if I could get the index of the collection that is being collected:
<Button x:Name="DeviceSelMenuButton" Content="{Binding DeviceID_and_SN, Mode=TwoWay}" HorizontalAlignment="Left" Height="28" Margin="25,103,0,0" VerticalAlignment="Top" Width="187" FontSize="14" Click="DeviceSelMenuButton_Click">
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding DeviceID_SN_Collection, Mode=TwoWay}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="IsCheckable" Value="true"/>
<Setter Property="Command" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}, Path=DataContext.MyCommand}"/>
<Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ContextMenu}}}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
As you can imagine, I have tried many variations for the CommandParameter bindings, but with this one I can at least see some things in my ICommand method. The thing that troubles me is that at the first push of the button (and no there are no errors in the Output window regarding the bindings), under the ContextMenu cm's Items property, I see a legitimate items label under items.CurrentItem, and item.CurrentPosition is 0 -- which at first looked promising, was hoping I could use that as the index, until I realized I was looking at the last item and so it must be meaningless. After that, the second time, and all subsequent pushes of the button, the items.CurrentItem is null, and the items.Current position is 0xffffffff. Pasting in the relevant pieces of code, starting with the class defining the collection, ICommand, etc:
class CustomDeviceGUI : INotifyPropertyChanged
{
// Declare the event
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _deviceDisplayString;
private ICommand UpdateMenuICommand;
List<string> ControllerDeviceList = new List<string>();
private System.Collections.ObjectModel.ObservableCollection<string> _DeviceID_SN_Collection = new System.Collections.ObjectModel.ObservableCollection<string>();
// CTOR
public CustomDeviceGUI()
{
ControllerDeviceList.Add("CustomDevice Device 1");
ControllerDeviceList.Add("CustomDevice Device 2");
ControllerDeviceList.Add("CustomDevice Device 3");
ControllerDeviceList.Add("CustomDevice Device 6");
UpdateDeviceID(3); // TODO Get from GUI!!!
}
#region CustomDeviceGUI Properties
public System.Collections.ObjectModel.ObservableCollection<string> DeviceID_SN_Collection
{
get
{
_DeviceID_SN_Collection.Clear();
foreach (string str in ControllerDeviceList)
{
_DeviceID_SN_Collection.Add(str);
}
return _DeviceID_SN_Collection;
}
private set
{
_DeviceID_SN_Collection = value;
}
}
public string DeviceID_and_SN
{
get
{
return _deviceDisplayString;
}
private set
{
_deviceDisplayString = value;
}
}
public ICommand MyCommand
{
get
{
if (UpdateMenuICommand == null)
UpdateMenuICommand = new MyGuiCommand();
return UpdateMenuICommand;
}
set
{
UpdateMenuICommand = value;
RaisePropertyChangeEvent("MyCommand"); // ????
}
}
public void UpdateDeviceID(int deviceID)
{
this._deviceDisplayString = ControllerDeviceList[deviceID];
RaisePropertyChangeEvent("DeviceID_and_SN");
RaisePropertyChangeEvent("DeviceID_SN_Collection");
}
public class MyGuiCommand : ICommand
{
// Two events are kicked off when the command is executed
public static event UpdateDeviceSelectedEventHandler UpdateDeviceSelectedEvent;
// defining signature for any event handlers for the events we create here
public delegate void UpdateDeviceSelectedEventHandler(int deviceIndex);
public void Execute(object parameter)
{
ContextMenu cm = (ContextMenu)parameter;
var itemSource = cm.ItemsSource;
var itemBG = cm.ItemBindingGroup;
var items = cm.Items;
UpdateDeviceSelectedEvent(1); // TODO parameter with index from GUI
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged // was ;
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
} // class CustomDeviceGUI
and finally the relevant code from MainWindow.xaml.cs, not a lot happening here:
// register for the event from the ICommand.Execute
WpfBindingAttempts.CustomDeviceGUI.MyGuiCommand.UpdateDeviceSelectedEvent += new WpfBindingAttempts.CustomDeviceGUI.MyGuiCommand.UpdateDeviceSelectedEventHandler(UpdateDeviceSelectedAfterSwitch);
// Handles event that occurs when a different device is selected
// via the dropdown menu -- sets the active device, and updates its ID/SN
void UpdateDeviceSelectedAfterSwitch(int deviceIndex)
{
_customDeviceGui.UpdateDeviceID(deviceIndex);
}
code behind for button:
private void DeviceSelMenuButton_Click(object sender, RoutedEventArgs e)
{
// " (sender as Button)" is PlacementTarget
(sender as Button).ContextMenu.IsEnabled = true;
(sender as Button).ContextMenu.PlacementTarget = (sender as Button);
(sender as Button).ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
(sender as Button).ContextMenu.IsOpen = true;
}
Think that's everything. Any help is greatly appreciated. If this looks familiar, it is. I got over the first hurdles and slammed into this brick wall.
I don't think there's any direct way to return the selected index of an item within a ContextMenu. I don't believe the CurrentItem property holds anything to do with which item has been selected. I'm not sure exactly what this property does do (it might be something used internally by the framework), but I would recommend that you ignore it.
Instead of making the context menu be the CommandParameter, make the items in your collection the CommandParameters. To do this, change the CommandParameter setter to the following:
<Setter Property="CommandParameter" Value="{Binding}" />
Then, pass the list of all devices to your MyGuiCommand, in a constructor argument for example. Finally, in your Execute method, search within this list of devices to find the selected device, which will be in the parameter.

ComboBox Binding to Custom ViewModel

I am working on a WPF project. This is my XAML code:
<Window x:Class="MyNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:l="clr-namespace:MyNamespace"
xmlns:p="clr-namespace:MyNamespace.Properties"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="500" Title="{x:Static p:Resources.Title}" Width="500"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<l:BrowsersViewModel x:Key="BrowsersViewModel"/>
</Window.Resources>
<Canvas Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
DataContext="{StaticResource BrowsersViewModel}">
<ComboBox Canvas.Left="10" Canvas.Top="10" DisplayMemberPath="Name"
ItemsSource="{Binding Path=Items}"
SelectedItem="{Binding Mode=TwoWay, Path=SelectedItem}"
SelectedValuePath="Process" Width="379"/>
<Button Content="Repopulate" Canvas.Right="10"
Canvas.Top="10" Width="75"/>
</Canvas>
</Window>
And this is my ViewModel code:
// BrowserInstance is a simple struct with two public fields:
// 1) System.Diagnostics.Process Process
// 2) System.String Name
public sealed class BrowsersViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private BrowserInstance m_SelectedItem;
public BrowserInstance SelectedItem
{
get { return m_SelectedItem; }
set
{
if (m_SelectedItem != value)
{
m_SelectedItem = value;
NotifyPropertyChanged("SelectedItem");
}
}
}
private ObservableCollection<BrowserInstance> m_Items;
public ObservableCollection<BrowserInstance> Items
{
get { return m_Items; }
set
{
if (m_Items != value)
{
m_Items = value;
NotifyPropertyChanged("Items");
}
}
}
public BrowsersViewModel()
{
m_Items = new ObservableCollection<BrowserInstance>();
Populate();
}
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, (new PropertyChangedEventArgs(propertyName)));
}
public void Populate()
{
foreach (Process process in Process.GetProcessesByName("chrome"))
{
BrowserInstance instance = new BrowserInstance();
instance.Process = process;
instance.Name = "[Chrome] "
+ process.Handle.ToString()
+ " " + ((process.MainWindowTitle.Length > 0) ?
process.MainWindowTitle : "NULL");
m_Items.Add(instance);
}
NotifyPropertyChanged("Items");
}
}
I'm having REALLY hard times getting this to work. I've looked at TONS of examples everywhere and I still can't find a solution to make everything work as expected.
1) I see a lot of values inside my ComboBox dropdown, but they are all empty. I would like to display the BrowserInstance.Name property inside the ComboBox and retrieve the BrowserInstance.Process value when an item is selected.
2) When the application starts, a check on the currently running browser processes is made in order to populate the ComboBox. If no running instances are found, how could I display a message inside my ComboBox like "No instances have been found!"?
3) If one or more browser instances are found when the application stats, how can I select the first one by default?
4) The Repopulate button will be used in order to recheck running browser instances by users. Let's say the previously selected instance is still running... how can I keep that one selected? And if the previously selected instances is not running anymore, how can I select by default the first one once again?
Many thanks!
EDIT: HERE IS MY CURRENT CODE
MainWindow:
public MainWindow()
{
InitializeComponent();
DataContext = m_BrowserInstances = new BrowserInstancesViewModel();
}
private void OnClickButtonRefresh(Object sender, RoutedEventArgs e)
{
m_BrowserInstances.Populate();
}
BrowserInstancesViewModel:
public void Populate()
{
BrowserInstance selectedItem = m_SelectedItem;
List<BrowserInstance> items = new List<BrowserInstance>();
foreach (Process process in Process.GetProcessesByName("chrome"))
items.Add(new BrowserInstance(process));
if (items.Count > 0)
{
m_Items = new ObservableCollection<BrowserInstance>(items.OrderBy(x => x.Process.Id));
if ((selectedItem != null) && (m_Items.SingleOrDefault(NewMethod(selectedItem)) != null))
m_SelectedItem = selectedItem;
else
m_SelectedItem = m_Items[0];
m_Enabled = true;
}
else
{
m_Items = new ObservableCollection<BrowserInstance>() { (new BrowserInstance()) };
m_SelectedItem = m_Items[0];
m_Enabled = false;
}
NotifyPropertyChanged("Enabled");
NotifyPropertyChanged("Items");
NotifyPropertyChanged("SelectedItem");
}
BrowserInstance is a simple struct with two public fields:
WPF Does not support Data Binding to fields. Only Properties.
This should work:
public class BrowserInstance
{
public string Name { get; set; }
public Process Process { get; set; }
}
1) I can't replicate this issue, are you sure there's a running chrome process? :D
2) you can hack it like this: :D
<ComboBox Canvas.Left="10" Canvas.Top="10"
DisplayMemberPath="Name" ItemsSource="{Binding Path=Items}"
SelectedItem="{Binding Mode=TwoWay, Path=SelectedItem}"
SelectedValuePath="Process" Width="200">
<ComboBox.Style>
<Style TargetType="ComboBox">
<Setter Property="IsEnabled" Value="True"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Items.Count}" Value="0">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<TextBlock Canvas.Left="10" Canvas.Top="10"
Text="No instances have been found!" >
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Items.Count}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
or better yet you retemplate the combobox to include textblock and bind its visibility using the hasitems property value of the combobox (ofcourse you will need to use BoolToVis converter)
3) add this code into the vm constructor after populate
if (m_Items.Count > 0)
{
SelectedItem = m_Items[0];
}
4) add new repopulate method into the vm and store the current selecteditem, populate, check whether exist or not, reselect the item
public void Repopulate()
{
BrowserInstance currentSelectedItem = m_SelectedItem;
Populate();
if (m_Items.Count>0)
{
if (currentSelectedItem !=null
&& m_Items.FirstOrDefault
((bi) => bi.Process == currentSelectedItem .Process)
!= null)
{
SelectedItem = currentSelectedItem;
}
else
{
SelectedItem = m_Items[0];
}
}
}
NB:
I'm not sure what property to check to verify the existence of the instance becoz when i tried above code and debug it my chrome' process list keep changing. but basically thats what you could do

Categories