EDIT: Problem was fixed in .NET 4.0.
I have been trying to bind a group of radio buttons to a view model using the IsChecked button. After reviewing other posts, it appears that the IsChecked property simply doesn't work. I have put together a short demo that reproduces the problem, which I have included below.
Here is my question: Is there a straightforward and reliable way to bind radio buttons using MVVM? Thanks.
Additional information: The IsChecked property doesn't work for two reasons:
When a button is selected, the IsChecked properties of other buttons in the group don't get set to false.
When a button is selected, its own IsChecked property does not get set after the first time the button is selected. I am guessing that the binding is getting trashed by WPF on the first click.
Demo project: Here is the code and markup for a simple demo that reproduces the problem. Create a WPF project and replace the markup in Window1.xaml with the following:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
<StackPanel>
<RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" />
<RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" />
</StackPanel>
</Window>
Replace the code in Window1.xaml.cs with the following code (a hack), which sets the view model:
using System.Windows;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = new Window1ViewModel();
}
}
}
Now add the following code to the project as Window1ViewModel.cs:
using System.Windows;
namespace WpfApplication1
{
public class Window1ViewModel
{
private bool p_ButtonAIsChecked;
/// <summary>
/// Summary
/// </summary>
public bool ButtonAIsChecked
{
get { return p_ButtonAIsChecked; }
set
{
p_ButtonAIsChecked = value;
MessageBox.Show(string.Format("Button A is checked: {0}", value));
}
}
private bool p_ButtonBIsChecked;
/// <summary>
/// Summary
/// </summary>
public bool ButtonBIsChecked
{
get { return p_ButtonBIsChecked; }
set
{
p_ButtonBIsChecked = value;
MessageBox.Show(string.Format("Button B is checked: {0}", value));
}
}
}
}
To reproduce the problem, run the app and click Button A. A message box will appear, saying that Button A's IsChecked property has been set to true. Now select Button B. Another message box will appear, saying that Button B's IsChecked property has been set to true, but there is no message box indicating that Button A's IsChecked property has been set to false--the property hasn't been changed.
Now click Button A again. The button will be selected in the window, but no message box will appear--the IsChecked property has not been changed. Finally, click on Button B again--same result. The IsChecked property is not updated at all for either button after the button is first clicked.
If you start with Jason's suggestion then the problem becomes a single bound selection from a list which translates very nicely to a ListBox. At that point it's trivial to apply styling to a ListBox control so that it shows up as a RadioButton list.
<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<RadioButton Content="{TemplateBinding Content}"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Looks like they fixed binding to the IsChecked property in .NET 4. A project that was broken in VS2008 works in VS2010.
For the benefit of anyone researching this question down the road, here is the solution I ultimately implemented. It builds on John Bowen's answer, which I selected as the best solution to the problem.
First, I created a style for a transparent list box containing radio buttons as items. Then, I created the buttons to go in the list box--my buttons are fixed, rather than read into the app as data, so I hard-coded them into the markup.
I use an enum called ListButtons in the view model to represent the buttons in the list box, and I use each button's Tag property to pass a string value of the enum value to use for that button. The ListBox.SelectedValuePath property allows me to specify the Tag property as the source for the selected value, which I bind to the view model using the SelectedValue property. I thought I would need a value converter to convert between the string and its enum value, but WPF's built-in converters handled the conversion without problem.
Here is the complete markup for Window1.xaml:
<Window x:Class="RadioButtonMvvmDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<!-- Resources -->
<Window.Resources>
<Style x:Key="RadioButtonList" TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}" >
<Setter Property="Margin" Value="5" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border BorderThickness="0" Background="Transparent">
<RadioButton
Focusable="False"
IsHitTestVisible="False"
IsChecked="{TemplateBinding IsSelected}">
<ContentPresenter />
</RadioButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border BorderThickness="0" Padding="0" BorderBrush="Transparent" Background="Transparent" Name="Bd" SnapsToDevicePixels="True">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<!-- Layout -->
<Grid>
<!-- Note that we use SelectedValue, instead of SelectedItem. This allows us
to specify the property to take the value from, using SelectedValuePath. -->
<ListBox Style="{StaticResource RadioButtonList}" SelectedValuePath="Tag" SelectedValue="{Binding Path=SelectedButton}">
<ListBoxItem Tag="ButtonA">Button A</ListBoxItem>
<ListBoxItem Tag="ButtonB">Button B</ListBoxItem>
</ListBox>
</Grid>
</Window>
The view model has a single property, SelectedButton, which uses a ListButtons enum to show which button is selected. The property calls an event in the base class I use for view models, which raises the PropertyChanged event:
namespace RadioButtonMvvmDemo
{
public enum ListButtons {ButtonA, ButtonB}
public class Window1ViewModel : ViewModelBase
{
private ListButtons p_SelectedButton;
public Window1ViewModel()
{
SelectedButton = ListButtons.ButtonB;
}
/// <summary>
/// The button selected by the user.
/// </summary>
public ListButtons SelectedButton
{
get { return p_SelectedButton; }
set
{
p_SelectedButton = value;
base.RaisePropertyChangedEvent("SelectedButton");
}
}
}
}
In my production app, the SelectedButton setter will call a service class method that will take the action required when a button is selected.
And to be complete, here is the base class:
using System.ComponentModel;
namespace RadioButtonMvvmDemo
{
public abstract class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Protected Methods
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">The name of the changed property.</param>
protected void RaisePropertyChangedEvent(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
}
#endregion
}
}
Hope that helps!
One solution is to update the ViewModel for the radio buttons in the setter of the properties. When Button A is set to True, set Button B to false.
Another important factor when binding to an object in the DataContext is that the object should implement INotifyPropertyChanged. When any bound property changes, the event should be fired and include the name of the changed property. (Null check omitted in the sample for brevity.)
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool _ButtonAChecked = true;
public bool ButtonAChecked
{
get { return _ButtonAChecked; }
set
{
_ButtonAChecked = value;
PropertyChanged(this, new PropertyChangedEventArgs("ButtonAChecked"));
if (value) ButtonBChecked = false;
}
}
protected bool _ButtonBChecked;
public bool ButtonBChecked
{
get { return _ButtonBChecked; }
set
{
_ButtonBChecked = value;
PropertyChanged(this, new PropertyChangedEventArgs("ButtonBChecked"));
if (value) ButtonAChecked = false;
}
}
}
Edit:
The issue is that when first clicking on Button B the IsChecked value changes and the binding feeds through, but Button A does not feed through its unchecked state to the ButtonAChecked property. By manually updating in code the ButtonAChecked property setter will get called the next time Button A is clicked.
Here is another way you can do it
VIEW:
<StackPanel Margin="90,328,965,389" Orientation="Horizontal">
<RadioButton Content="Mr" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
<RadioButton Content="Mrs" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
<RadioButton Content="Ms" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
<RadioButton Content="Other" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}}" GroupName="Title"/>
<TextBlock Text="{Binding SelectedTitle, Mode=TwoWay}"/>
</StackPanel>
ViewModel:
private string selectedTitle;
public string SelectedTitle
{
get { return selectedTitle; }
set
{
SetProperty(ref selectedTitle, value);
}
}
public RelayCommand TitleCommand
{
get
{
return new RelayCommand((p) =>
{
selectedTitle = (string)p;
});
}
}
Not sure about any IsChecked bugs, one possible refactor you could make to your viewmodel:the view has a number of mutually exclusive states represented by a series of RadioButtons, only one of which at any given time can be selected. In the view model, just have 1 property (e.g. an enum) which represents the possible states: stateA, stateB, etc That way you wouldn't need all the individual ButtonAIsChecked, etc
A small extension to John Bowen's answer: It doesn't work when the values don't implement ToString(). What you need instead of setting the Content of the RadioButton to a TemplateBinding, just put a ContentPresenter in it, like this:
<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<RadioButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}">
<ContentPresenter/>
</RadioButton>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
This way you can additionally use DisplayMemberPath or an ItemTemplate as appropriate. The RadioButton just "wraps" the items, providing the selection.
I know this is an old question and the original issue was resolved in .NET 4. and in all honesty this is slightly off topic.
In most cases where I've wanted to use RadioButtons in MVVM it's to select between elements of an enum, this requires binding a bool property in the VM space to each button and using them to set an overall enum property that reflects the actual selection, this gets very tedious very quick. So I came up with a solution that is re-usable and very easy to implement, and does not require ValueConverters.
The View is pretty much the same, but once you have your enum in place the VM side can be done with a single property.
MainWindowVM
using System.ComponentModel;
namespace EnumSelectorTest
{
public class MainWindowVM : INotifyPropertyChanged
{
public EnumSelectorVM Selector { get; set; }
private string _colorName;
public string ColorName
{
get { return _colorName; }
set
{
if (_colorName == value) return;
_colorName = value;
RaisePropertyChanged("ColorName");
}
}
public MainWindowVM()
{
Selector = new EnumSelectorVM
(
typeof(MyColors),
MyColors.Red,
false,
val => ColorName = "The color is " + ((MyColors)val).ToString()
);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The class that does all the work inherits from DynamicObject. Viewed from the outside it creates a bool property for each element in the enum prefixed with 'Is', 'IsRed', 'IsBlue' etc. that can be bound to from XAML. Along with a Value property that holds the actual enum value.
public enum MyColors
{
Red,
Magenta,
Green,
Cyan,
Blue,
Yellow
}
EnumSelectorVM
using System;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
namespace EnumSelectorTest
{
public class EnumSelectorVM : DynamicObject, INotifyPropertyChanged
{
//------------------------------------------------------------------------------------------------------------------------------------------
#region Fields
private readonly Action<object> _action;
private readonly Type _enumType;
private readonly string[] _enumNames;
private readonly bool _notifyAll;
#endregion Fields
//------------------------------------------------------------------------------------------------------------------------------------------
#region Properties
private object _value;
public object Value
{
get { return _value; }
set
{
if (_value == value) return;
_value = value;
RaisePropertyChanged("Value");
_action?.Invoke(_value);
}
}
#endregion Properties
//------------------------------------------------------------------------------------------------------------------------------------------
#region Constructor
public EnumSelectorVM(Type enumType, object initialValue, bool notifyAll = false, Action<object> action = null)
{
if (!enumType.IsEnum)
throw new ArgumentException("enumType must be of Type: Enum");
_enumType = enumType;
_enumNames = enumType.GetEnumNames();
_notifyAll = notifyAll;
_action = action;
//do last so notification fires and action is executed
Value = initialValue;
}
#endregion Constructor
//------------------------------------------------------------------------------------------------------------------------------------------
#region Methods
//---------------------------------------------------------------------
#region Public Methods
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
string elementName;
if (!TryGetEnumElemntName(binder.Name, out elementName))
{
result = null;
return false;
}
try
{
result = Value.Equals(Enum.Parse(_enumType, elementName));
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException)
{
result = null;
return false;
}
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object newValue)
{
if (!(newValue is bool))
return false;
string elementName;
if (!TryGetEnumElemntName(binder.Name, out elementName))
return false;
try
{
if((bool) newValue)
Value = Enum.Parse(_enumType, elementName);
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException)
{
return false;
}
if (_notifyAll)
foreach (var name in _enumNames)
RaisePropertyChanged("Is" + name);
else
RaisePropertyChanged("Is" + elementName);
return true;
}
#endregion Public Methods
//---------------------------------------------------------------------
#region Private Methods
private bool TryGetEnumElemntName(string bindingName, out string elementName)
{
elementName = "";
if (bindingName.IndexOf("Is", StringComparison.Ordinal) != 0)
return false;
var name = bindingName.Remove(0, 2); // remove first 2 chars "Is"
if (!_enumNames.Contains(name))
return false;
elementName = name;
return true;
}
#endregion Private Methods
#endregion Methods
//------------------------------------------------------------------------------------------------------------------------------------------
#region Events
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion Events
}
}
To respond to changes you can either subscribe to the NotifyPropertyChanged event or pass an anonymous method to the constructor as done above.
And finally the MainWindow.xaml
<Window x:Class="EnumSelectorTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel>
<RadioButton IsChecked="{Binding Selector.IsRed}">Red</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsMagenta}">Magenta</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsBlue}">Blue</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsCyan}">Cyan</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsGreen}">Green</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsYellow}">Yellow</RadioButton>
<TextBlock Text="{Binding ColorName}"/>
</StackPanel>
</Grid>
</Window>
Hope someone else finds this useful, 'cause I reckon this ones going in my toolbox.
You have to add the Group Name for the Radio button
<StackPanel>
<RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" GroupName="groupName" />
<RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" GroupName="groupName" />
</StackPanel>
I have a very similar problem in VS2015 and .NET 4.5.1
XAML:
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="6" Rows="1"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate >
<RadioButton GroupName="callGroup" Style="{StaticResource itemListViewToggle}" Click="calls_ItemClick" Margin="1" IsChecked="{Binding Path=Selected,Mode=TwoWay}" Unchecked="callGroup_Checked" Checked="callGroup_Checked">
....
As you can see in this code i have a listview, and items in template are radiobuttons that belongs to a groupname.
If I add a new item to the collection with the property Selected set to True it appears checked and the rest of buttons remain checked.
I solve it by getting the checkedbutton first and set it to false manually but this is not the way it's supposed to be done.
code behind:
`....
lstInCallList.ItemsSource = ContactCallList
AddHandler ContactCallList.CollectionChanged, AddressOf collectionInCall_change
.....
Public Sub collectionInCall_change(sender As Object, e As NotifyCollectionChangedEventArgs)
'Whenever collection change we must test if there is no selection and autoselect first.
If e.Action = NotifyCollectionChangedAction.Add Then
'The solution is this, but this shouldn't be necessary
'Dim seleccionado As RadioButton = getCheckedRB(lstInCallList)
'If seleccionado IsNot Nothing Then
' seleccionado.IsChecked = False
'End If
DirectCast(e.NewItems(0), PhoneCall).Selected = True
.....
End sub
`
<RadioButton IsChecked="{Binding customer.isMaleFemale}">Male</RadioButton>
<RadioButton IsChecked="{Binding customer.isMaleFemale,Converter= {StaticResource GenderConvertor}}">Female</RadioButton>
Below is the code for IValueConverter
public class GenderConvertor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !(bool)value;
}
}
this worked for me. Even value got binded on both view and viewmodel according to the radio button click. True--> Male and False-->Female
Related
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>
Here is my firt question for Stackoverflow, I hope that will be ok!
I'm working on a custom Dropdown Button in WPF, and I would like to add a click event on the buttons "Text1" and "Text2". I have to put this dropdown button in a DLL so I use the WPF CustomControl library. So in the perfect world, I would like to create several methods in the MainWindow.xaml.cs and send the name of the method in a class where the name of the button, the icon , the tooltip, ... that will be used in the generic.xaml to find the method to call.
I hope what I said is clear :3
The purpose of this is to have a reusable dropdown button where I can add some click event in the items when we click on it.
Here is the generic.xaml with my dropdown button :
<Style TargetType="{x:Type local:ButtonDropdown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ButtonDropdown}">
<mah:DropDownButton Content="{Binding Path=Text, RelativeSource={RelativeSource TemplatedParent}}"
ToolTip="{Binding Path=ToolTip, RelativeSource={RelativeSource TemplatedParent}}"
x:Name="DropDownButton"
Orientation="Vertical"
BorderThickness="0"
ItemsSource="{Binding ItemsSource}">
<mah:DropDownButton.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0" ToolTip="{Binding Tooltip}">
<StackPanel.InputBindings>
<MouseBinding Command="{Binding Path=SomeCommand, RelativeSource={RelativeSource TemplatedParent}}" MouseAction="LeftClick" />
</StackPanel.InputBindings>
<Image Source="{Binding Icon}" Width="16"></Image>
<TextBlock Text="{Binding Text}" x:Name="PART_DropdownButton">
</TextBlock>
</StackPanel>
</DataTemplate>
</mah:DropDownButton.ItemTemplate>
<mah:DropDownButton.Icon>
<Image Source="{Binding Path=Icon, RelativeSource={RelativeSource TemplatedParent}}" Width="32"></Image>
</mah:DropDownButton.Icon>
</mah:DropDownButton>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The call of this custom dropdown in the MainWindow.xaml :
<CustomButton:ButtonDropdown Text="Dropdown"
x:Name="ButtonDropdown"
Icon="Images/Open.png"
ToolTip="TOOLTIP DROPDOWN"
ItemsSource="{Binding Items}"/>
Here is my method OnApplyTemplate I add the line 'TextBlock textblock= GetTemplateChild("PART_DropdownButton") as TextBlock;' after the first answer.
public override void OnApplyTemplate()
{
DropDownButton dropDownButton = GetTemplateChild("DropDownButton") as DropDownButton;
TextBlock textblock= GetTemplateChild("PART_DropdownButton") as TextBlock;
textblock.MouseDown += Method1;
dropDownButton.ItemsSource = DropdownItems;
dropDownButton.Click += ButtonDropdown_Click;
}
And finally the class I have created for items in the dropdown :
public class DropdownItem
{
private string text;
private string icon;
private string tooltip;
private string clickEvent;
}
For the moment I have try with command and mousedown on textblock but don't work :/
Edit : I add the name for the textBlock and I add my method OnApplyTemplate from my ButtonDropdown.cs. The dropDownButton.click is ok but when I try to get the "PART_DropdownButton" that is null. I think because of there is not only one but several textBlock so he don't know which one to take. But that is my problem how to asign a different method on all textblock.mouseDown ? How can we put a different name on all textblock ?
Assuming your Dropdown Button derives from a button control give the DropDown button a name in the xaml file e.g. "PART_DropdownButton". Then reference the name in the code behind in the OnApplyTemplate procedure. Here you can add an event handler trapping your mouse events.
private DropdownButton dropdownbutton = null;
...
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
dropdownbutton = base.GetTemplateChild("PART_DropdownButton") as ToggleButton;
if (dropdownbutton != null)
{
dropdownbutton.MouseDown += MouseDown_Click;
}
else
....;
}
Next write your event handler for MouseDown_Click.
Regards Martin
I finally find something that works like I want !
I add an Icommand in my dropdownItem. That will contain my method.
public class DropdownItem
{
private string text;
private string icon;
private string tooltip;
private string clickEvent;
public ICommand ClickCommand { get; set; }
}
In my MainWindow.xaml.cs I add the command I need.
private ICommand _command1;
private ICommand _command2;
public MainWindow()
{
InitializeComponent();
Items.Add(new DropdownItem("Text1", "Images/Open.png", "Method1", "TEST")
{
ClickCommand = Command1
});
Items.Add(new DropdownItem("Text2", "Images/Open.png", "method2", "TEST2")
{
ClickCommand = Command2
});
ButtonDropdown.DropdownItems = Items;
}
public ICommand Command1
{
get
{
return _command1 = new RelayCommand(Method1);
}
}
public ICommand Command2
{
get
{
return _command2 = new RelayCommand(Method2);
}
}
public void Method1()
{
MessageBox.Show("Method 1");
}
public void Method2()
{
MessageBox.Show("Method 2");
}
And finally I add the call to this method in my generic.xaml
<MouseBinding Command="{Binding ClickCommand}" MouseAction="LeftClick" />
Thanks for your help, that's because of your comments and answers that I understood that I was looking in the bad direction
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.
I am trying to bind the "IsChecked" property on the ToggleButton to "ModelView.IsEnabled".
"ModelView.IsEnabled" is always "false"
but somehow the ToggleButton can still show as "Checked".
Is there anything wrong with the binding?
XAML
...
<Page.Resources>
<ModelView:ModelView x:Key="ModelView"/>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ToggleButton IsChecked="{Binding Source={StaticResource ModelView}, Path=IsEnabled, Mode=TwoWay}">
<TextBlock >UWP Toggle Button</TextBlock>
</ToggleButton>
</Grid>
...
ModelView.cs
using...
namespace App2
{
class ModelView : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler CanExecuteChanged;
private bool _isEnabled;
public bool IsEnabled
{
get {
return _isEnabled;
}
set
{
_isEnabled = false;
OnPropertyChanged("IsEnabled");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
Try this, it worked to me:
1. Xaml code changes:
<Grid>
<Grid.DataContext>
<soHelpProject:MainViewModel/>
</Grid.DataContext>
<ToggleButton IsChecked="{Binding IsToggled, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<TextBlock >UWP Toggle Button</TextBlock>
</ToggleButton>
</Grid>
regards,
In your class ModelView, change IsEnabled from this:
public bool IsEnabled
{
get {
return _isEnabled;
}
set
{
_isEnabled = false;
OnPropertyChanged("IsEnabled");
}
}
to this:
public bool IsEnabled
{
get {
return _isEnabled;
}
set
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
EDIT: If i use _isEnabled = !value; as you suggested, it still works, with button and state now showing opposite values:
EDIT 2: Now, if you want to properly test your binding, then you could add an extra regular button and do this:
private void button1_Click(object sender, RoutedEventArgs e)
{
myModelView.IsEnabled = !myModelView.IsEnabled;
}
so you can watch your ToggleButton switch between true and false every time you click Test Button. Please note that Test Button is not bound to anything, it's just for testing purposes. See corresponding XAML at the bottom.
The problem is that the way you're doing it, "forcing" IsEnabled to be always false, you're actually sabotaging your own code...:O)
And finally, it is not clear from your code when/where you're assigning your DataContext. Please see below how to do it.
XAML:
<Page.DataContext>
<local:MyModelView/>
</Page.DataContext>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ToggleButton x:Name="toggleButton1" Content="ToggleButton" IsChecked="{Binding IsEnabled, Mode=TwoWay}" HorizontalAlignment="Center"/>
<TextBlock x:Name="textBlock1" Text="{Binding IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="126,0,201,286" />
<Button x:Name="button1" Click="button1_Click" Margin="127,400,0,220" Content="Test Button" Height="35" />
</Grid>
Code-behind:
private void Page_Loaded(object sender, RoutedEventArgs e)
{
myModelView = new MyModelView();
this.DataContext = myModelView;
}
I've run into the same problem, be it not with a ToggleButton, but with a TextBox, where I wanted to format the text the user had entered.
In your case you want to change the IsChecked property in your viewmodel and have it reflected in the User Interface straight away (so always be unchecked). The reason you want that is of absolutely no importance.
The problem is that with UWP the getter of your property gets called as you would expect when you click the ToggleButton. The normal action for the ToggleButton is to change from unchecked to checked (and vice versa) and that is what happens in your case. But then you expect that NotifyPropetyChanged signals the control in the UI. And that's where it goes wrong. The getter never gets called when the setter is executed (including NotifyPropertyChanged), so the UI doesn't reflect what you did in your setter.
This is very different from what the TwoWay Binding used to do (and still does in WPF). So there is nothing wrong with your code, but it seems that the binding mechanism has changed, although Microsoft claims it didn't. If you would use x:Bind, it works fine, so hat might solve your problem.
To clarify things more I have taken your example and modified it slightly, to show the problem.
I've put a ToggleButton on the page with a TwoWay binding to a viewmodel, exactly as you did. Clicking on the ToggleButton will switch its state from checked to unchecked and vice versa, even though the setter in my viewmodel Always sets the property to false (so unchecked).
But I've also added a normal button, that I've bound to a command that also modifies the property that the ToggleButton is bound to. Clicking this button calls the setter on the property the ToggleButton is bound to. Of course the setter gets called just the same, but after that the binding to the ToggleButton gets called, so NotifyPropertyChanged in this case does cause a UI update.
If you use the debugger, you can see exactly what i mean.
So your problem can be solved by using x:Bind, or by figuring out another way to update the UI, which you shouldn't have to do if Binding was still working as it used to. Maybe Microsoft has implemented some kind of optimization that now destroys classic Binding.
No special things, just a MainPage and a viewmodel.
My code for MainPage.xaml
<Page x:Class="App10.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:App10"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<local:ViewModel x:Key="viewModel" />
</Page.Resources>
<Grid x:Name="mainGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Margin="10,20,10,0">
<Button
x:Name="Button"
Content="UWP Normal button"
Command="{Binding Source={StaticResource viewModel}, Path=SwitchIschecked}"
HorizontalAlignment="Stretch" />
<ToggleButton
x:Name="toggleButton"
Margin="0,10,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
IsChecked="{Binding Source={StaticResource viewModel}, Path=IsChecked,
Mode=TwoWay}">
<TextBlock>UWP Toggle Button</TextBlock>
</ToggleButton>
</StackPanel>
</Grid>
</Page>
The code for MainPage.xaml.cs
using Windows.UI.Xaml.Controls;
// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace App10
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
}
}
And the code for ViewModel.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace App10
{
public class ViewModel : INotifyPropertyChanged
{
private bool _isChecked;
// property for TwoWay binding with ToggleButton
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
// extra var just to check 'value'
var _value = value;
// now always set it to false
_isChecked = false;
// Try to pass value of _isChecked to user interface
// because there is no check whether the value really
// has changed
// But this only works if the setter is not being called
// directly from the control the property is bound to
OnPropertyChanged();
}
}
private ICommand _switchChecked;
// ICommand for normal button, binding to Command
// calls method to set Property for ToggleButton
public ICommand SwitchIschecked
{
get
{
if ( _switchChecked == null )
_switchChecked = new ChangeChecked( new Action( ChangeVar ));
return _switchChecked;
}
set
{
_switchChecked = value;
}
}
// This will set the property for the ToggleButton
private void ChangeVar()
{
IsChecked = !IsChecked;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged( [CallerMemberName] string propertyName = null )
{
var handler = PropertyChanged;
handler?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}
}
/// <summary>
/// Quick class to implement ICommand
/// </summary>
class ChangeChecked : ICommand
{
Action _execute;
public ChangeChecked( Action execute )
{
_execute = execute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute( object parameter )
{
return true;
}
public void Execute( object parameter )
{
_execute();
}
}
}
IsEnabled property is indicating whether the user can interact with the control. IsPressed is readonly property. So IsChecked is probably what you need.
I'm working on developing a custom user control for my application. This control is very simple. It's just a grid, with a checkbox in [0,0] and a TextBlock in [0,1]. I've had no issues getting it designed how I'd like in XAML.
However, the second step is giving me some trouble. I'm trying to expose the IsChecked bool? of my sub-control that is a Checkbox for binding on my mainform, and the same idea with the Text property of TextBlock.
I've tried a few different ways of going about this, but to no avail.
Here's the general code I have:
public partial class CDCheckBox : UserControl
{
public bool? IsChecked
{
get { return chk.IsChecked; }
set { chk.IsChecked = value; }
}
public string Text
{
get { return lbl.Text; }
set { lbl.Text = value; }
}
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register(
"IsChecked",
typeof(bool?),
typeof(CDCheckBox),
new PropertyMetadata(default(bool?), OnItemsPropertyChanged));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(CDCheckBox),
new PropertyMetadata(default(string), OnItemsPropertyChanged));
/*
public event PropertyChangedEventHandler PropertyChanged;
private void OnNotify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
*/
private static void OnItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// AutocompleteTextBox source = d as AutocompleteTextBox;
// Do something...
//lbl.Text = e.NewValue.ToString();
}
/*
public event PropertyChangedEventHandler PropertyChanged;
private void OnNotify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
*/
public CDCheckBox()
{
InitializeComponent();
}
}
When I run the code above, I get no errors, but my binded data doesn't show up in my TextBlock control. When I tried before I wrote the depenency properties, it gave me an error in my XAML saying "A 'Binding' cannot be set on the 'IsChecked' property of type 'CDCheckBox'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject."
Interestingly however, this error does not appear in the constructor, but instead in the window_loaded method I've written. This appears to be a red herring however, as if I comment out that code, it still fails before the form can display with XAMLParse Error.
Further to my comment, you could try styling an existing control that has the property types that you need. For example, in your custom control you have a nullable Boolean property and a string property. If you repurpose a CheckBox control, it already has a nullable Boolean property (IsChecked) and an object property (Content) which can be used to hold a string.
Here's how you might restyle a CheckBox control and change its template to achieve the result you're after:
<Window x:Class="..."
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<Style x:Key="MySuperCheckboxStyle"
TargetType="{x:Type CheckBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
IsChecked="{TemplateBinding IsChecked}"
Content="Whatever you need here" />
<TextBlock Grid.Column="1"
Text="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel>
<CheckBox IsChecked="True"
Content="Unstyled check box"
Margin="10" />
<CheckBox Style="{StaticResource MySuperCheckboxStyle}"
IsChecked="True"
Content="Styled check box"
Margin="10" />
</StackPanel>
</Window>
Key here are the TemplateBinding bindings used in the control template. These bind not to a data context like in normal data binding, but rather to properties of the control being templated.
Whenever you find yourself wanting to create a custom control in WPF it is worth exploring whether you can take an existing control and change its appearance to suit what you need, as this is often less work than creating a new control (on the flipside it's not always possible to repurpose an existing control, particularly if you need different behaviour).