ComboBox Binding to Custom ViewModel - c#

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

Related

WPF Toggle Combo Box Item Text Colour MVVM ReactiveUI

OK so this is definitely a newbie question that unfortunately could not figure/find the answer to.
Essentially binding a list of objects to a Combobox, when the Disabled property on the object is set to true I want the text colour of the Combobox item to be set to gray.
This is what I have so far:
Combobox item datatype
public class ListItem
{
public ListItem(string text)
{
Text = text;
}
public string Text { get; set; }
public bool Disabled { get; set; }
}
Viewmodel setup
public class MainPageViewModel : ReactiveObject
{
// In ReactiveUI, this is the syntax to declare a read-write property
// that will notify Observers, as well as WPF, that a property has
// changed. If we declared this as a normal property, we couldn't tell
// when it has changed!
private ListItem _selectedItem;
public ListItem SelectedItem
{
get => _selectedItem;
set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
}
public List<ListItem> Items { get; set; }
public MainPageViewModel()
{
Items = new List<ListItem>
{
new ListItem ("A Cat"),
new ListItem ("A Dog"),
new ListItem ("A Mouse"),
new ListItem ("A Frog") { Disabled = true }
};
}
}
ReactiveUI Binding
public MainPage()
{
InitializeComponent();
ViewModel = new MainPageViewModel();
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Items, v => v.MyComboBox.ItemsSource)
.DisposeWith(d);
this.Bind(ViewModel, vm => vm.SelectedItem, v => v.MyComboBox.SelectedItem)
.DisposeWith(d);
});
}
Xaml markup
<ComboBox
Name="MyComboBox"
Margin="0,0,0,20"
Foreground="black">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Disabled}" Value="True">
<Setter Property="Foreground" Value="Gray" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
Any help is appreciated let me know if you need more information.
Solution: It looks like in future I need to test the example code before puttin it up - our actual code had the Disabled property set as a readonly which must mess with WPF binding. Changing it to public set and get solved the first issue of not seeing it greyed out! It would seem staring at a problem for so long blinds you and it really is that simple.
As for graying out the selected item I will try it out and see.
The last item in the dropdown already has its text grayed out, so I assume you're asking about the selected item. The ComboBox uses separate data templates for the selected item and the items in the dropdown. You can use a DataTemplateSelector to set both.
public class ComboBoxTemplateSelector : DataTemplateSelector
{
public DataTemplate SelectedItemTemplate { get; set; }
public DataTemplate DropdownItemsTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var itemToCheck = container;
// Search up the visual tree, stopping at either a ComboBox or a ComboBoxItem (or null).
// This will determine which template to use.
while (itemToCheck is not null and not ComboBox and not ComboBoxItem)
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
// If you stopped at a ComboBoxItem, you're in the dropdown.
return itemToCheck is ComboBoxItem ? DropdownItemsTemplate : SelectedItemTemplate;
}
}
Xaml markup
<StackPanel>
<StackPanel.Resources>
<Style x:Key="GrayedOutText" TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Disabled}" Value="True">
<Setter Property="Foreground" Value="Gray" />
</DataTrigger>
</Style.Triggers>
</Style>
<local:ComboBoxTemplateSelector x:Key="ComboBoxTemplateSelector">
<local:ComboBoxTemplateSelector.SelectedItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Style="{StaticResource GrayedOutText}" />
</DataTemplate>
</local:ComboBoxTemplateSelector.SelectedItemTemplate>
<local:ComboBoxTemplateSelector.DropdownItemsTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Style="{StaticResource GrayedOutText}" />
</DataTemplate>
</local:ComboBoxTemplateSelector.DropdownItemsTemplate>
</local:ComboBoxTemplateSelector>
</StackPanel.Resources>
<ComboBox
Name="MyComboBox"
Margin="0,0,0,20"
ItemTemplateSelector="{StaticResource ComboBoxTemplateSelector}">
</ComboBox>
</StackPanel>
We have some repetition in the DataTemplate definitions, but these tend to grow apart in production code.
Resources
Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?
https://www.reactiveui.net/docs/getting-started/compelling-example
I'm assuming your problem is that ComboBoxItems do not get grayed once the app is running.
I'm not familiar with ReactiveUI, but since I found a problem in your code, I tried it in a CommunityToolkit.Mvvm version of your code and verified my theory.
Bottom of line, you need to implement the ReactiveUI version of INotifyPropertyChanged to the Disabled property.
If you are interested in, I can post the CommunityToolkit.Mvvm version of this code.
Here is an approach that worked in my tests:
Combobox item datatype:
//-- Unchanged
public class ListItem
{
public ListItem( string text )
{
Text = text;
}
public string Text { get; set; }
public bool Disabled { get; set; }
}
Viewmodel setup:
public class MainPageViewModel : INotifyPropertyChanged
{
private ListItem? _selectedItem;
public event PropertyChangedEventHandler? PropertyChanged;
public ListItem? SelectedItem
{
get => _selectedItem;
set
{
//-- I didn't had the "RaiseAndSetIfChanged" method, so I just implemented the functionality manually
if( value != _selectedItem )
{
//-- Update the value ...
_selectedItem = value;
//-- ... AND inform everyone (who is interested) about the change
this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( nameof( this.SelectedItem ) ) );
}
}
}
//-- Use always an ObservableCollection when you want to achieve reactivity
public ObservableCollection<ListItem> Items
{ get; } = new ObservableCollection<ListItem>();
public MainPageViewModel()
{
//-- Add some test data
this.Items.Add( new ListItem( "A Cat" ) );
this.Items.Add( new ListItem( "A Dog" ) );
this.Items.Add( new ListItem( "A Mouse" ) );
this.Items.Add( new ListItem( "A Frog" ) { Disabled = true } );
//-- Just select the first item
this.SelectedItem = this.Items[0];
}
}
Main page:
public MainPage()
{
//-- Define the DataContext BEFORE the UI will be initialized ;)
this.DataContext = new MainPageViewModel();
InitializeComponent();
//-- Never saw such code before -> just don't do that ;)
//this.WhenActivated( d =>
//{
// this.OneWayBind( ViewModel, vm => vm.Items, v => v.MyComboBox.ItemsSource )
// .DisposeWith( d );
// this.Bind( ViewModel, vm => vm.SelectedItem, v => v.MyComboBox.SelectedItem )
// .DisposeWith( d );
//} );
}
Xaml markup:
<DockPanel>
<ComboBox
DockPanel.Dock="Top"
Name="MyComboBox"
Margin="0,0,0,20"
Foreground="black"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" />
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Disabled}" Value="True">
<Setter Property="Foreground" Value="Gray" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
<!-- Details View -->
<StackPanel>
<!-- name -->
<StackPanel Orientation="Horizontal">
<Label Content="Item Name" />
<TextBox Text="{Binding SelectedItem.Text}" />
</StackPanel>
<!-- disabled flag -->
<StackPanel Orientation="Horizontal">
<Label Content="IsDisabled" />
<CheckBox IsChecked="{Binding SelectedItem.Disabled}" />
</StackPanel>
</StackPanel>
</DockPanel>
I hope this will satisfy your requirements. Have fun :)

DataGrid using MVVM Context Menu items depend on whether a Row is Selected or not

I'm updating a small app to MVVM. It uses a DataGrid and the context menu displayed depend upon whether a row is selected or not. My problem is when I right click on an unselected row the row is first selected so the Selected Menu is displayed. The left mouse button is coded to toggle the selection. If I toggle the selection off then click the right mouse button the No Selection Menu is displayed. How can I get the right mouse click to show the no selection menu on an unselected row without having to explicitly deselect the row?
I have created a small example to illustrate the problem.
I'm using Fody to handle property changes; Microsoft MVVM Toolkit.
XAML:
<Window.Resources>
<ContextMenu x:Key="PopupNoSelection">
<MenuItem Name="NewItem" Header="No Item Selection" Command="{Binding NoSelectionClick}" />
</ContextMenu>
<ContextMenu x:Key="PopupSelection">
<MenuItem Name="AmmendItem" Header="Item Selection" Command="{Binding SelectionClick}" />
</ContextMenu>
</Window.Resources>
<Grid>
<DataGrid x:Name="dgToggle" AutoGenerateColumns="False"
CanUserSortColumns="False" CanUserAddRows="False" SelectionMode="Single"
SelectedItem="{Binding ToggleSelected, Mode=TwoWay}" SelectionUnit="FullRow"
ItemsSource="{Binding ToggleRows}">
<DataGrid.Style>
<Style TargetType="{x:Type DataGrid}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=SelectedItems.Count, RelativeSource={RelativeSource Self}}" Value="0">
<Setter Property="ContextMenu" Value="{StaticResource ResourceKey=PopupNoSelection}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=SelectedItems.Count, RelativeSource={RelativeSource Self}}" Value="1">
<Setter Property="ContextMenu" Value="{StaticResource ResourceKey=PopupSelection}" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Style>
<DataGrid.Columns>
<DataGridTextColumn Header="Rubbish" Binding="{Binding Rubbish, Mode=OneWay}" />
</DataGrid.Columns>
<!-- Toggle Selected item & set Selected Item Colour -->
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="DoSelectedRow" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="LimeGreen" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
XAML.cs
public partial class DataGridToggleView : Window
{
public DataGridToggleView()
{
InitializeComponent();
}
public void DoSelectedRow(object sender, MouseButtonEventArgs e)
{
DataGridCell cell = sender as DataGridCell;
if (cell != null && !cell.IsEditing)
{
DataGridRow row = FindVisualParent<DataGridRow>(cell);
if (row != null)
{
row.IsSelected = !row.IsSelected;
e.Handled = true;
}
}
}
public static Parent FindVisualParent<Parent>(DependencyObject child) where Parent : DependencyObject
{
DependencyObject parentObject = child;
while (!((parentObject is System.Windows.Media.Visual)
|| (parentObject is System.Windows.Media.Media3D.Visual3D)))
{
if (parentObject is Parent || parentObject == null)
{
return parentObject as Parent;
}
else
{
parentObject = (parentObject as FrameworkContentElement).Parent;
}
}
parentObject = VisualTreeHelper.GetParent(parentObject);
if (parentObject is Parent || parentObject == null)
{
return parentObject as Parent;
}
else
{
return FindVisualParent<Parent>(parentObject);
}
}
}
Model:
public class DataGridToggleModel
{
public string Rubbish { get; set; }
}
ViewModel
public class DataGridToggleViewModel : ObservableObject
{
public ObservableCollection<DataGridToggleModel> ToggleRows { get; set; }
public List<DataGridToggleModel> ToggleList { get; set; }
private DataGridToggleModel _toggleSelected;
public DataGridToggleModel ToggleSelected
{
get { return _toggleSelected; }
set
{
_toggleSelected = value;
}
}
public ICommand MouseLeftClick { get; set; }
public ICommand NoSelectionClick { get; set; }
public ICommand SelectionClick { get; set; }
public DataGridToggleViewModel()
{
MouseLeftClick = new RelayCommand<object>(MouseLeftCommand);
NoSelectionClick = new RelayCommand(NoSelectionCommand);
SelectionClick = new RelayCommand(SelectionCommand);
}
private void MouseLeftCommand( object obj)
{
////MessageBox.Show($"Row Clicked {ToggleSelected.Rubbish}");
//DataGridRow dataGridRow = ToggleSelected as DataGridRow;
//ToggleSelected.IsSelected = !ToggleSelected.IsSelected;
}
private void NoSelectionCommand()
{
MessageBox.Show("No Selection Menu");
}
private void SelectionCommand()
{
MessageBox.Show("Selected Menu");
}
public void PopulateDataGrid()
{
ToggleList = new List<DataGridToggleModel>();
ToggleList.Add(new DataGridToggleModel { Rubbish = "Waste paper" });
ToggleList.Add(new DataGridToggleModel { Rubbish = "Empty tins" });
ToggleList.Add(new DataGridToggleModel { Rubbish = "Junk mail" });
ToggleList.Add(new DataGridToggleModel { Rubbish = "Empty boxes" });
ToggleList.Add(new DataGridToggleModel { Rubbish = "Newpapers & Magazines" });
ToggleList.Add(new DataGridToggleModel { Rubbish = "Old clothes" });
ToggleList.Add(new DataGridToggleModel { Rubbish = "Packaging" });
ToggleRows = new ObservableCollection<DataGridToggleModel>(ToggleList);
}
}
Any help would be appreciated - thanks.

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 set a Binding from ItemTemplate to the hosting Container in an ItemsControl? (UWP)

Given an arbitrary ItemsControl, like a ListView, I want to set a Binding from inside the ItemsTemplate to the hosting Container. How can I do that easily? For example, in WPF we can do it using this inside the ItemTemplate
<ListView.ItemTemplate>
<DataTemplate>
<SomeControl Property="{Binding Path=TargetProperty, RelativeSouce={RelativeSource FindAncestor, AncestorType={x:Type MyContainer}}}" />
</DataTemplate>
<ListView.ItemTemplate>
In this example (for WPF) the Binding will be set between Property in SomeControl and TargetProperty of the ListViewItem (implicit, because it will be generated dynamically by the ListView to host the each of its items).
How can we do achieve the same in UWP?
I want something that is MVVM-friendly. Maybe with attached properties or an Interaction Behavior.
When the selection changes, search the visual tree for the radio button with the DataContext corresponding to selected/deselected items. Once it's found, you can check/uncheck at your leisure.
I have a toy model object looking like this:
public class Data
{
public string Name { get; set; }
}
My Page is named self and contains this collection property:
public Data[] Data { get; set; } =
{
new Data { Name = "One" },
new Data { Name = "Two" },
new Data { Name = "Three" },
};
The list view, binding to the above collection:
<ListView
ItemsSource="{Binding Data, ElementName=self}"
SelectionChanged="OnSelectionChanged">
<ListView.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The SelectionChanged event handler:
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListView lv = sender as ListView;
var removed = FindRadioButtonWithDataContext(lv, e.RemovedItems.FirstOrDefault());
if (removed != null)
{
removed.IsChecked = false;
}
var added = FindRadioButtonWithDataContext(lv, e.AddedItems.FirstOrDefault());
if (added != null)
{
added.IsChecked = true;
}
}
Finding the radio button with a DataContext matching our Data instance:
public static RadioButton FindRadioButtonWithDataContext(
DependencyObject parent,
object data)
{
if (parent != null)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
ListViewItem lv = child as ListViewItem;
if (lv != null)
{
RadioButton rb = FindVisualChild<RadioButton>(child);
if (rb?.DataContext == data)
{
return rb;
}
}
RadioButton childOfChild = FindRadioButtonWithDataContext(child, data);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return null;
}
And finally, a helper method to find a child of a specific type:
public static T FindVisualChild<T>(
DependencyObject parent)
where T : DependencyObject
{
if (parent != null)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
T candidate = child as T;
if (candidate != null)
{
return candidate;
}
T childOfChild = FindVisualChild<T>(child);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return default(T);
}
The result:
This will break if a given model instance shows up more than once in the list.
Note: this answer is based on WPF, there might be some changes necessary for UWP.
There are basically two cases to consider:
You have a data driven aspect that needs to be bound to the item container
You have a view-only property
Lets assume a customized listview for both cases:
public class MyListView : ListView
{
protected override DependencyObject GetContainerForItemOverride()
{
return new DesignerItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is DesignerItem;
}
}
public class DesignerItem : ListViewItem
{
public bool IsEditing
{
get { return (bool)GetValue(IsEditingProperty); }
set { SetValue(IsEditingProperty, value); }
}
public static readonly DependencyProperty IsEditingProperty =
DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem));
}
In case 1, you can use the ItemContainerStyle to link your viewmodel property with a binding and then bind the same property inside the datatemplate
class MyData
{
public bool IsEditing { get; set; } // also need to implement INotifyPropertyChanged here!
}
XAML:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
<local:MyListView.ItemContainerStyle>
<Style TargetType="{x:Type local:DesignerItem}">
<Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</local:MyListView.ItemContainerStyle>
<local:MyListView.ItemTemplate>
<DataTemplate>
<Border Background="Red" Margin="5" Padding="5">
<CheckBox IsChecked="{Binding IsEditing}"/>
</Border>
</DataTemplate>
</local:MyListView.ItemTemplate>
</local:MyListView>
In case 2, it appears that you don't really have a data driven property and consequently, the effects of your property should be reflected within the control (ControlTemplate).
In the following example a toolbar is made visible based on the IsEditing property. A togglebutton can be used to control the property, the ItemTemplate is used as an inner element next to the toolbar and button, it knows nothing of the IsEditing state:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
<local:MyListView.ItemContainerStyle>
<Style TargetType="{x:Type local:DesignerItem}">
<Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DesignerItem}">
<DockPanel>
<ToggleButton DockPanel.Dock="Right" Margin="5" VerticalAlignment="Top" IsChecked="{Binding IsEditing,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" Content="Edit"/>
<!--Toolbar is something control related, rather than data related-->
<ToolBar x:Name="MyToolBar" DockPanel.Dock="Top" Visibility="Collapsed">
<Button Content="Tool"/>
</ToolBar>
<ContentPresenter ContentSource="Content"/>
</DockPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsEditing" Value="True">
<Setter TargetName="MyToolBar" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</local:MyListView.ItemContainerStyle>
<local:MyListView.ItemTemplate>
<DataTemplate>
<Border Background="Red" Margin="5" Padding="5">
<TextBlock Text="Hello World"/>
</Border>
</DataTemplate>
</local:MyListView.ItemTemplate>
</local:MyListView>
For a better control template, you may chose to use Blend and create the control template starting at the full ListViewItem template and just editing your changes into it.
If your DesignerItem generally has a specific enhanced appearance, consider designing it in the Themes/Generic.xaml with the appropriate default style.
As commented, you could provide a separate data template for the editing mode. To do this, add a property to MyListView and to DesignerItem and use MyListView.PrepareContainerForItemOverride(...) to transfer the template.
In order to apply the template without the need for Setter.Value bindings, you can use value coercion on DesignerItem.ContentTemplate based on IsEditing.
public class MyListView : ListView
{
protected override DependencyObject GetContainerForItemOverride()
{
return new DesignerItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is DesignerItem;
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var elem = element as DesignerItem;
elem.ContentEditTemplate = ItemEditTemplate;
}
public DataTemplate ItemEditTemplate
{
get { return (DataTemplate)GetValue(ItemEditTemplateProperty); }
set { SetValue(ItemEditTemplateProperty, value); }
}
public static readonly DependencyProperty ItemEditTemplateProperty =
DependencyProperty.Register("ItemEditTemplate", typeof(DataTemplate), typeof(MyListView));
}
public class DesignerItem : ListViewItem
{
static DesignerItem()
{
ContentTemplateProperty.OverrideMetadata(typeof(DesignerItem), new FrameworkPropertyMetadata(
null, new CoerceValueCallback(CoerceContentTemplate)));
}
private static object CoerceContentTemplate(DependencyObject d, object baseValue)
{
var self = d as DesignerItem;
if (self != null && self.IsEditing)
{
return self.ContentEditTemplate;
}
return baseValue;
}
private static void OnIsEditingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(ContentTemplateProperty);
}
public bool IsEditing
{
get { return (bool)GetValue(IsEditingProperty); }
set { SetValue(IsEditingProperty, value); }
}
public static readonly DependencyProperty IsEditingProperty =
DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsEditingChanged)));
public DataTemplate ContentEditTemplate
{
get { return (DataTemplate)GetValue(ContentEditTemplateProperty); }
set { SetValue(ContentEditTemplateProperty, value); }
}
// Using a DependencyProperty as the backing store for ContentEditTemplate. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContentEditTemplateProperty =
DependencyProperty.Register("ContentEditTemplate", typeof(DataTemplate), typeof(DesignerItem));
}
Note, for an easier example I will activate the "edit" mode by ListViewItem.IsSelected with some trigger:
<local:MyListView ItemsSource="{Binding Source={StaticResource items}}">
<local:MyListView.ItemContainerStyle>
<Style TargetType="{x:Type local:DesignerItem}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="IsEditing" Value="True"/>
</Trigger>
</Style.Triggers>
</Style>
</local:MyListView.ItemContainerStyle>
<local:MyListView.ItemTemplate>
<DataTemplate>
<Border Background="Red" Margin="5" Padding="5">
<TextBlock Text="Hello World"/>
</Border>
</DataTemplate>
</local:MyListView.ItemTemplate>
<local:MyListView.ItemEditTemplate>
<DataTemplate>
<Border Background="Green" Margin="5" Padding="5">
<TextBlock Text="Hello World"/>
</Border>
</DataTemplate>
</local:MyListView.ItemEditTemplate>
</local:MyListView>
Intended behavior: the selected item becomes edit-enabled, getting the local:MyListView.ItemEditTemplate (green) instead of the default template (red)
Just in case you might want to have an IsSelected property in your view model item class, you may create a derived ListView that establishes a Binding of its ListViewItems to the view model property:
public class MyListView : ListView
{
public string ItemIsSelectedPropertyName { get; set; } = "IsSelected";
protected override void PrepareContainerForItemOverride(
DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
BindingOperations.SetBinding(element,
ListViewItem.IsSelectedProperty,
new Binding
{
Path = new PropertyPath(ItemIsSelectedPropertyName),
Source = item,
Mode = BindingMode.TwoWay
});
}
}
You might now simply bind the RadioButton's IsChecked property in the ListView's ItemTemplate to the same view model property:
<local:MyListView ItemsSource="{Binding DataItems}">
<ListView.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Content}"
IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</local:MyListView>
In the above example the data item class also has Content property. Obviously, the IsSelected property of the data item class must fire a PropertyChanged event.

Binding dynamically created menu items to ICommand from Button.ContextMenu

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.

Categories