WPF KeyBinding disable and let bubble up keyboard shortcut - wpf

I am working on a project which uses MVVM, KeyBinding and ICommand.
I have, on the same window, multiple nested Views (UserControls) and many of them uses the same KeyBinding "Ctrl+S" to run a SaveCommand.
The ViewModel associated with the View, has a IsSaveCommandAvailable property, that can tell if the SaveCommand is available in that ViewModel.
In my case, only the "root" View has to be able to launch the SaveCommand by hitting Ctrl+S, the nested ones have to ignore the key hit and let it bubble up to the root View, that does all the Save stuff.
I googled for finding a solution, and only found that I can use ICommand.CanExecute to return false and avoid the KeyBinding to run.
But this solution does not fit my needs, because if I hit Ctrl+S on a child View, its SaveCommand CanExecute returns false, and the key hit is lost.
Is there a way to bubble up the key hit until a KeyBinding can be run?

The solution I found is to use a IValueConverter on the Key property of KeyBinding, converting a boolean value to the key passed as CommandParameter, and, if the value is false, return Key.None:
public class BooleanToKeyConverter : IValueConverter
{
/// <summary>
/// Key to use when the value is false
/// </summary>
public Key FalseKey { get; set; } = Key.None;
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is bool flag && flag && parameter != null && parameter != DependencyProperty.UnsetValue)
{
if (parameter is Key key)
{
return key;
}
else if (Enum.TryParse<Key>(parameter.ToString(), out var parsedKey))
{
return parsedKey;
}
}
return this.FalseKey;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
In a resource file (eg: App.xaml):
<conv:BooleanToKeyConverter x:Key="boolToKey"/>
where "conv" is your local namespace.
Then, in KeyBindings:
<KeyBinding Command="{Binding Path=SaveCommand}"
Key="{Binding Path=IsSaveCommandAvailable, Converter={StaticResource boolToKey}, ConverterParameter=S}"
Modifiers="Ctrl"/>

If you prefer to leave Key property as it is (and bindable), you can derive KeyBinding and add IsEnabled dependency property while also overriding Gesture:
public override InputGesture Gesture
{
get
{
return IsEnabled ? base.Gesture as KeyGesture : new KeyGesture(Key.None);
}
set
{
base.Gesture = value;
}
}
// IsEnabled dependency property changed callback
private static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as MyKeyBinding)?.WritePostscript(); // raise Gesture changed
}
dont forget to notify UI that Gesture changed in IsEnabled changed callback using WritePostscript() as well as to override CreateInstanceCore.
Usage example:
<utils:MyKeyBinding
Command="{Binding SaveCommand}"
Key="{Binding KeyBindings.SaveKey}"
Modifiers="{Binding KeyBindings.SaveModifiers}"
IsEnabled="{Binding IsSaveCommandAvailable}"/>

Related

Bind Visibility to ReactiveCommand CanExecute

I have several Tiles (TileLayoutControl Class) in my xaml (only shown 2 in this example) whose Visibility are binded to Boolean Properties and converted through BooleanToVisibilityConverter.
This works just fine. My question is
Can I bind the visibility to the Command instead so that I can remove the need of those several Boolean Properties?
Something like binding the Visibility to Command.CanExecute
If Yes, How can I achieve that? Any help will be really appreciated! Thanks.
<dxlc:Tile Command="{Binding Tile1Command}"
Visibility="{Binding Path=IsTile1Visible , Converter={StaticResource BooleanToVisibilityConverter}}"/>
<dxlc:Tile Command="{Binding Tile2Command}"
Visibility="{Binding Path=IsTile2Visible , Converter={StaticResource BooleanToVisibilityConverter}}"/>
ViewModel
private bool _isTile1Visible;
public bool IsTile1Visible
{
get { return _isTile1Visible; }
set { this.RaiseAndSetIfChanged(ref _isTile1Visible, value); }
}
public ReactiveCommand Tile1Command { get; private set; }
Tile1Command = new ReactiveCommand();
Tile1Command.Subscribe(p => PerformTile1Operation());
Yes, just use RxUI bindings:
<dxlc:Tile x:Name="Tile1" />
Then in your View constructor (make sure to implement IViewFor<Tile1ViewModel> to get this extension):
this.BindCommand(ViewModel, x => x.Tile1Command);
this.WhenAnyObservable(x => x.ViewModel.Tile1Command.CanExecuteObservable)
.BindTo(this, x => x.Tile1.Visibility);
You could also solve this in the ViewModel level, though that's not what I would do - in the ViewModel ctor:
Tile1Command = new ReactiveCommand(/* ... */);
Tile1Command
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
.ToProperty(this, x => x.Tile1Visibility, out tile1Visibility);
ReactiveCommand is an ICommand implementation that is simultaneously a RelayCommand implementation...
Assume that the ReactiveCommand has been declared like this...
public ReactiveCommand FileCommand { get; private set; }
...and has been instantiated in a View Model like this...
SomeText = "";
FileCommand = new ReactiveCommand(this.WhenAny(vm => vm.SomeText, s => !string.IsNullOrWhiteSpace(s.Value)));
FileCommand.Subscribe(param => MessageBox.Show("Processing"));
... which means if the property SomeText is empty, then the command cannot be executed, otherwise the command can be executed. And if the command is executed, a message box will get displayed.
If your objective is to simply eliminate the boolean IsTile1Visible, you can make a Xaml declaration like this...
<Button Content="File"
Command="{Binding FileCommand}"
Visibility="{Binding FileCommand, Converter={genericMvvm1:CommandToVisibilityConverter}}" />
where the visibility is bound to the same command and uses a value converter...
and the value converter looks like this...
public class CommandToVisibilityConverter : MarkupExtension, IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
ICommand iCommand = value as ICommand;
if (iCommand != null)
{
if (iCommand.CanExecute(parameter))
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
}
catch
{
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
The value converter simply dereferences the command into a basic ICommand and converts it into a visibility. Note that since this converter inherits from Markup Extension, there's no need to declare it as a static resource in the Xaml's object graph.
NOTE: the same functionality can be achieved by using 'code-behind' available in ReactiveUI, but the Xaml/ValueConverter appeals to developers who do not want their View Models to deal explicitly with the 'Visibility' property.
You could potentially do that, but it would require subclassing the command so that it also implements INotifyPropertyChanged, and the underlying condition would need to raise PropertyChange for the CanExecute property whenever it changes.
It won't work without that, as ICommand doesn't implement INotifyPropertyChanged - it uses CanExecuteChanged instead.
Note that you could simplify the property, however, by just handling it yourself in the constructor:
// In constructor:
Tile1Command = new ReactiveCommand();
Tile1Command.Subscribe(p => PerformTile1Operation());
IReactiveObject self = this as IReactiveObject;
Tile1Command.CanExecuteChanged += (o,e) => self.RaisePropertyChanged(new PropertyChangedEventArgs("IsTile1Visible"));
Then your property becomes:
// Use command directly here...
public bool IsTile1Visible
{
get { return Tile1Command.CanExecute; }
}

WPF Databinding with ObservableCollection to Label

I have a ObservableCollection which I need to bind to 2 labels, first to show count of items in the collection and second to show the sum of values.
First label is bound to collections count property and second label is bound directly to ObservableCollection with a convertor to calculate total of all items
XAML looks something like this
<Grid>
<ListBox Name="itemList" ItemsSource="{Binding DataList}"/>
<Label Name="lblcount" Content="{Binding DataList.Count}" />
<Label Name="lblTotal" Content="{Binding DataList, Converter={StaticResource calculateTotalConvertor}" />
</Grid>
My VM has a collection like this
ObservableCollection<int> data = new ObservableCollection<int>();
public ObservableCollection<int> DataList
{
get { return data; }
set { data = value; }
}
My convertor code is
public class CalculateTotalConvertor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<int> collection = value as ObservableCollection<int>;
return collection.Sum();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Issue is on adding new items in DataList, ListView and label showing count of items gets updated but "lblTotal" doesnt get updated with total count.
Basically how to force your binding to be evaluated on ObservableCollection changes ? How does it work directly for ListView or DataGrid but not for label ?
I know this problem can be solved by creating a property in VM to show total and raise property change when collection gets updated but is there is any better solution than that ?
Of-course this is simplified form of my actual problem, I dont have access to the ViewModel and the collection, its a third party control. I am creating a wrapper user control and have a relative binding with the view to its inner collection.
The other answers correctly explain why it is not updating. To force it to update you can change your converter to an IMultiValueConverter:
public class CalculateTotalConvertor : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<int> collection = values.FirstOrDefault() as ObservableCollection<int>;
return collection.Sum();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then change your binding to a MultiBinding which also pulls in the Count:
<Label Name="lblTotal">
<Label.Content>
<MultiBinding Converter="{StaticResource calculateTotalConvertor}">
<Binding Path="DataList"/>
<Binding Path="DataList.Count"/>
</MultiBinding>
</Label.Content>
</Label>
Now the second binding will notify that the binding needs to update when items are added or removed, but you can just ignore the count value and not use it.
Its not updating because its bound to DataList and DataList has not changed, The count label updates because its bound to DataList.Count which is updated when an item is added to the list.
The only way I can think of to update the Sum label is to notify the UI that the DataList has changed, but this will cause the ListBox to rebind the list and it will performace will be a lot more expensive than just having a property on your model update the Sum.
So I think the best option would be to use a property on your model to caculate the sum using the ObservableCollections CollectionChangedEvent or in the logic that adds items to the list
It works for ListView and DataGrid, because these are ItemsControls that listen to the ObservableCollection's CollectionChangedEvent, which is raised when the collection itself is changed by adding or removing items.
The Label on the other hand is a ContentControl that only listens to the PropertyChangedEvent. Since your DataList is the same ObservableCollection after the insertion as it was before, no events are raised.
Just saw your edit:
If you are creating a wrapping control, give the 3rd party control a name and hook up to its inner collection's CollectionChangedEvent from your control's code behind. That way you can still push update notifications to your wrapping view.
Go with the extra property, it will save you some code on the converter. From the code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
ObservableCollection<int> _list = new ObservableCollection<int>();
int _sum = 0;
Random rnd = new Random();
public MainWindow()
{
DataList = new ObservableCollection<int>();
DataList.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(DataList_CollectionChanged);
DataContext = this;
InitializeComponent();
}
void DataList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (object number in e.NewItems)
_sum += (int)number;
break;
case NotifyCollectionChangedAction.Remove:
foreach (object number in e.OldItems)
_sum -= (int)number;
break;
}
OnNotifyPropertyChanged("Sum");
}
public int Sum { get { return _sum; } }
public ObservableCollection<int> DataList { get; set; }
private void Add_Btn_Click(object sender, RoutedEventArgs e)
{
DataList.Add(rnd.Next(0, 256));
}
private void Remove_Btn_Click(object sender, RoutedEventArgs e)
{
if (DataList.Count == 0)
return;
DataList.RemoveAt(DataList.Count - 1);
}
public event PropertyChangedEventHandler PropertyChanged;
void OnNotifyPropertyChanged(string property)
{
if (PropertyChanged == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}

Silverlight Databinding Question

I have a TextBox ( TextBoxConsumer ) and i would like to enable a button in my UI when the length of the TextBox.Text is greater than 3,
i digged it down to
IsEnabled="{Binding
ElementName=TextBoxConsumer,
Path=Text.Length}"
for my button's IsEnabled Property but im not sure how to find the length and convert it to bool depending on the length of the text box how do i do it?
i would like to do it entirely in Xaml instead of code using Binding instead of code
I would prefer to use an IValueConverter class for this. I'll provide some quick code though its not exactly what you are looking for you should be able to tweak it.
In a cs file by itself:
using System;
using System.Globalization;
using System.Windows.Data;
public class IntCorrectAnswerToTrueFalseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value > 0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? 1 : 0;
}
}
In App.xaml, add this line to the ResourceDictionary:
<app:IntCorrectAnswerToTrueFalseConverter x:Key="IntCorrectAnswerToTrueFalseConverter" />
Then in the xaml of where you use it:
<CheckBox
x:Name="answerCheckBox"
IsChecked="{Binding Score, Converter={StaticResource IntCorrectAnswerToTrueFalseConverter}}"
Click="CheckBoxChecked"/>
I did something similar using a tutorial similar to this using the INotifyPropertyChanged interface. I assume you have a model you are using for binding to the UI. You have a string member (like TextBoxConsumerString) which binds to you textbox. Now you need to add a boolean like TextBoxConsumerEnabled which you will set inside of the setter of TextBoxConsumerString and call the notify changed method.
this.OnPropertyChanged( new PropertyChangedEventArgs( "TextBoxConsumerEnabled" ) );
Here is an example:
public class TextBoxConsumerModel : INotifyPropertyChanged
{
private string _textBoxConsumerString;
public event PropertyChangedEventHandler PropertyChanged;
public string TextBoxConsumerString
{
get
{
return _textBoxConsumerString;
}
set
{
if (_textBoxConsumerString == value)
return;
TextBoxConsumerEnabled = value != null && value.Length > 3;
_textBoxConsumerString = value;
OnPropertyChanged(new PropertyChangedEventArgs("TextBoxConsumerEnabled"));
}
}
public bool TextBoxConsumerEnabled { get; set; }
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}
That should be it as far as the model goes. Now you just need to bind to the two model properties from the XAML.

Getting 'ConvertBack(...)' to fire on WPF ComboBox selection change

I have a ComboBox with its Items property bound to a collection of objects. I also have SelectedItem property bound to the entire collection, with a ValueConverter designed to examine the elements in the collection and return the 1 item to be selected. This part works.
What doesn't work is when the user makes a selection change on the ComboBox, the ConvertBack(...) method of the ValueConverter is not being called. I need ConvertBack(...) called because I need to take the user's selection, re-examine the collection, and edit the old selected item and newly selected item appropriately.
I know this approach is a awkward, but it's the way it is. Here's the relevant code:
ComboBox:
<ComboBox ItemsSource="{Binding}" SelectedItem="{Binding Path=., Converter={StaticResource ResourceKey=DataInputAssetChoiceSelectedItemConverter}}" />
ValueConverter:
public class DataInputAssetChoiceSelectedItemConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
foreach (CustomObject Choice in (Collection<CustomObject>)value)
{
if (Choice.IsSelected)
{
return Choice;
}
}
return ((Collection<CustomObject>)value).First();
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ //breakpoint...execution never gets here!
return null;
}
}
So why doesn't ConvertBack(...) ever get called? Is it just something I'm misunderstanding about ComboBox? I've tried this approach using SelectedItem, SelectedValue, SelectedIndex, and have tried messing with UpdateSourceTrigger, various binding modes, DataTriggers, and can never seem to get ConvertBack(...) to be called. Is using the SelectionChanged event the only option? If so, why?
You are not binding to a property, so the Binding can't set anything. You are binding directly to the DataContext object, and the Binding won't update that.
If you had {Binding Path=SomeProperty, Converter=...} then the ConvertBack would be called. As it stands though, it won't be called.
You're right, it is awkward, but only because you're trying to add some management to the collection in a value converter instead of on the collection itself. I think it would help if your collection was more aware that its items have an IsSelected property:
public CustomCollection : Collection<CustomObject> {
CustomObject _current;
public CustomObject CurrentSelection {
get { return _current; }
set {
if (_current == value)
return;
if (_current != null)
_current.IsSelected = false;
if (value != null)
value.IsSelected = true;
_current = value;
}
}
}
Just add a little extra to make sure that _current is at least the first element in the collection.
<ComboBox ItemsSource="{Binding}" SelectedItem="{Binding CurrentSelection}">
Now you shouldn't need the converter anymore. There are some considerations missing, however. You may want to use ObservableCollection<T> instead and raise the PropertyChanged event when CurrentSelection is changed, so that if anything else is bound to that property or it changed in code, all bindings will update appropriately.
Edit: Wrapping the model
One easy way to wrap the collection instead of making a custom collection like above:
public class CollectionWrapper : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged = (o,e)=>{};
// never have to check for null
public CollectionWrapper(Collection<CustomObject> collection) {
Items = collection;
}
// unlikely to change, so let's prevent it for now
public Collection<CustomObject> Items {
get;
private set;
}
CustomObject _current;
public CustomObject CurrentSelection {
get { return _current; }
set {
if (_current == value)
return;
if (_current != null)
_current.IsSelected = false;
if (value != null)
value.IsSelected = true;
_current = value;
PropertyChanged(this, new PropertyChangedEventArgs("CurrentSelection"));
}
}
}
Then this object becomes your data context and the ComboBox bindings change to this:
<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding CurrentSelection}">

Setting Visibility using MVVM pattern in silverlight

I take one grid in silverlight. Initially textbox2 is invisible. When I click on textbox1 we have to visible textbox2. I try it as belows:
<TextBox x:Name="textbox1" SelectionChanged="txt1_SelectionChanged"/>
<TextBox x:Name="textbox2 " Visibility="Collapsed"/>
private void txt1_SelectionChanged(object sender, RoutedEventArgs e)
{
textbox2 .Visibility = Visibility.Visible;
}
It works fine.
But I want to use MVVM pattern. So there I don't want to use eventHandler.
So how to do that using MVVM pattern?
edit: sorry, i thought you meant the textbox to be visible when the other one has focus, I changed my inital answer.
I can not try it at the moment, but you bind the Visibility property of your textbox to the SelectionLength property of the other, using a valueconverter:
<UserControl.Resources>
<local:IntToVisibilityConverter x:Key="IntToVisibilityConverter" />
</UserControl.Resources>
<Textbox
x:name="textbox2"
Visibility={Binding SelectionLength,
ElementName="textbox1"
Converter={StaticResource IntToVisibilityConverter}}
/>
implement the value converter like this:
public class IntToVisibilityConverter : IValueConverter
{
public Object Convert(Object value, Type targetType, Object parameter, CultureInfo culture)
{
return (int)value > 0 ? Visibility.Visible : Visibility.Hidden;
}
public Object ConvertBack(Object value, Type targetType, Object parameter, CultureInfo culture)
{
throw new InvalidOperationException("Converter cannot convert back.");
}
}
Create a property in viewmodel
public bool IsVisible
{
get
{
return _isVisible;
}
set
{
if (_isVisible == value)
{
return;
}
_isVisible = value;
RaisePropertyChanged("IsVisible");
}
}
This returns a boolean value so u need a converter BoolToVisibility Converter make BoolToVisibilityConverter class
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (parameter == null)
{
return ((bool)value == true) ? Visibility.Visible : Visibility.Collapsed;
}
else if (parameter.ToString() == "Inverse")
{
return ((bool)value == true) ? Visibility.Collapsed : Visibility.Visible;
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
Now Bind the TextBox and use the converter
<UserControl.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</UserControl.Resources>
<Textbox x:name="textbox2" Visibility={Binding IsVisible,
Converter={StaticResource BoolToVisibilityConverter}}/>
This is it.
The biggest problem you will have is getting the SelectionChanged event sent to the ViewModel. Commands in SL4 only work on button clicks, so TextBox SelectionChanged events can't fire commands by default.
There are a few solutions out there for you:
Binding Commands to ANY event
EventToCommand Behavior
Once you have done that, you can have a command in your ViewModel that sets a Visibility property in your ViewModel and fires the PropertyChanged event.
Using my ViewModelSupport library, the VM would look like this:
public class MyViewModel : ViewModelBase
{
public Visibility ShowTextbox2
{
get { return Get(() => ShowTextbox2, Visibility.Collapsed); }
set { Set(() => ShowTextbox2, value); }
}
public void Execute_SelectionChanged()
{
ShowTextbox2 = Visibility.Visible;
}
}
}
You would then bind the SelectionChanged event to the SelectionChanged command in the VM and the Textbox2 visibility attribute to the ShowTextbox2 property in the VM.
Good luck.
If you are using MVVM Light, you can also do it like this:
using System.Windows; //place it at the top of your view model class.
private Visibility _isVisible = Visibility.Collapsed;
public Visibility IsVisible
{
get
{ return _isVisible; }
set
{
if (_isVisible == value) { return; }
RaisePropertyChanging(() => IsVisible);
_passwordMissing = value;
RaisePropertyChanged(() => IsVisible);
}
}

Resources