Explanation
Aim: In my wpf desktop application I want to add AccessText to a button that is bound to a command that has a predicate which evaluates if the window / control is busy and thus enables/disables the button.
Status: The button command works fine and is properly enabled prior to adding AccessText. (I'm using MvvmLight for my ICommand support.)
Problem: After adding AccessText the command still binds but the CanExecute seems to no longer get queried and the button is always enabled. (My ideal solution would be no code-behind just XAML.)
Example
XAML:
Working:
<Button Command="{Binding NavToStoresSearchCmd}" Content="Stores" Height="30"/>
Not Working:
<Button Command="{Binding NavToStoresSearchCmd}" Height="30">
<AccessText>S_tores</AccessText>
</Button>
C# (ViewModel):
public ICommand NavToStoresSearchCmd { get => new RelayCommand(OnNavToStoresSearch, () => IsNotBusy); }
(The code for IsNotBusy and the OnNavToStoresSearch command work fine, as does CanExecute UNTIL I add the AccessText.)
Having just tested this, it appears to be working as expected.
My code:
XAML:
<Button Command="{Binding BrowseCommand}">
<AccessText>_Browse</AccessText>
</Button>
C# (ViewModel):
In constructor:
BrowseCommand = new RelayCommand( BrowseCommandHandler, () => CanBrowse );
'CanBrowse' property:
private bool _canBrowse;
public bool CanBrowse
{
get { return _canBrowse; }
set { _canBrowse = value; BrowseCommand.RaiseCanExecuteChanged(); }
}
My guess is you're not calling 'RaiseCanExecuteChanged()' on your RelayCommand?
(Edit: Just seen the comments come in before I hit submit. Sorry folks!)
As said in the comments, RaiseCanExecuteChanged is needed here. WPF's CommandManager calls CanExecute when it detects UI changes, such binding updates, state changes, etc. It is unreliable. You may have been lucky in your earlier testing in how the CommandManager was reacting for you, but (in my opinion) it's better to explicitly call RaiseCanExecuteChanged when you know it's changed.
(Further edit)
I did a quick bit of searching around and it appears you can get around needing to call 'RaiseCanExecuteChanged' by changing a namespace entry. Change...
using GalaSoft.MvvmLight.Command;
for
using GalaSoft.MvvmLight.CommandWpf;
I quickly ripped the 'RaiseCanExecuteChanged()' code out from my test app and shockingly it appears to work.
Maybe I've been doing it wrong all these years...
Related
I'm fairly new to WPF. I have the following radio button in my application
<Viewbox Height="30">
<RadioButton Content="B1" GroupName="InputBelt" IsChecked="{Binding RBChecked, Mode=TwoWay, FallbackValue=True}" VerticalAlignment="Center"/>
</Viewbox>
<Viewbox Height="30">
<RadioButton Content="B2" GroupName="InputBelt" IsChecked="{Binding RBChecked, Converter={StaticResource boolconverter}, Mode=TwoWay}" VerticalAlignment="Center"/>
</Viewbox>
I have defined datacontext in xaml file
<Window.DataContext>
<vm:TestViewModel />
</Window.DataContext>
The issue is when the page is loaded for the 1st time, everything is fine. But when I go to some other page in the application and comes back to this page, the application crashes due to stackoverflow exception.
I even tried adding datacontext locally in radiobutton tag but it isn't working either.
Property structure given below.
private bool _bRBChecked;
public bool RBChecked
{
get { return _bRBChecked; }
set
{
_bRBChecked = value;
RaisePropertyChanged("RBChecked");
}
}
Upon investigating further, I found out that the RaisePropertyChanged of the binded property is being called too many times. This issue occurs only with the property binded to radio button. I have other controls which has two-way bind with other properties in the same page and it seems to work fine.
Now I have tried the below fix suggested in another stackoverflow question and it seems to be working.
set
{
if (_bRBChecked != value)
{
_bRBChecked = value;
RaisePropertyChanged("RBChecked");
}
}
But I would like to know the root cause of this issue and why the property is being set so many times and find any alternate fix if possible. Please let me know if I am missing anything.
Any insight is highly appreciable.
Your change notification is not protected from recursion. Property A changing Property B, whose change changes Property A...
A simple solution is this:
set
{
if(value != _bRBChecked){
_bRBChecked = value;
RaisePropertyChanged("RBChecked");
}
}
Simply check if the value is actually a change, before you go tell everyone about it. This pattern is explicitly used in the Examples. I am 90% sure the Depdency Properties have a similar recursion protection, but it would not be the first time I was wrong.
I think it is fairly easy to figure this out, based on the fix you shared.
What happens in steps:
You set the new value in one of the radio buttons
The event is raised
Since it's two way binding, the binding of the second radio button sets the value again to the other radio button
The event is raised again due to 3
Go back to 1 as now the value is set again for the first radio button.
With your fix the value is not set (the setter it's not called) so the event is not triggered again.
I'm inexperienced with both WPF and MVVM so i'm most likeley missing something but when I click my button the command isn't firing. I also have some menu controls on my page that i've setup the exact same way and when I click those, their commands work as expected.
I've tried attaching a click event handler to make sure the button is definitely being clicked which it is. I've also tried attaching a different command that works on my menu control which didn't work on the button.
<Button Grid.Row="1" Content="Add Note"
Command="{Binding InsertNoteCommand}"/>
public ICommand InsertNoteCommand { get; }
public MainViewModel()
{
InsertNoteCommand = new RelayCommand(InsertNote);
}
private void InsertNote()
{
Console.WriteLine("Note Inserted!");
}
I should also mention that i'm using MVVM Light
The debugging information is very useful to know but in the end I solved the problem by pointing the binding to the data context.
<Button x:Name="AddNewNoteBtn" Grid.Row="1" Content="Add Note"
Command="{Binding Path=DataContext.InsertNoteCommand, ElementName=_window}"/>
If anybody has comments on how I can improve this I would really appreciate it. Thanks!
I'm sorry for this being so wordy, but I want to make the situation perfectly clear. Please, if you are a WPF pro, take a look.
There are two CollectionViewSource bound to ItemsControls that use UserControl Views to display either StackPanels or custom Buttons. There is one for each side shown in the screenshot below. The problem I'm encountering is that when the parent collection property is set, all of the buttons in the DataTemplate view are disabled. Even the 2 buttons higher up are having the same problem even though they worked before my recent edits.
If I click on the form, or press any key, the buttons enable. As soon as the property is reset to a newly edited and sorted collection, they disable again. Rinse and repeat. This is what it looks like. The first frame is how it starts (gray using StackPanel), the 2nd is what it looks like when the RFS button is clicked, and the 3rd frame is what happens when I click anywhere or press a key.
I've been going in circles trying out things. The only thing that seems to work is a code-behind workaround that sets focus to one thing and then back. However, that would not be good for the user if they are trying to use one of the other dashboard items.
Since the WPF for all of this is very massive, I'll try to include just the relevant parts. These are the ItemsControls on the TabItemControl (UserControl).
<!-- BID (SELL) DEPTH -->
<ItemsControl Grid.Row="0" Grid.Column="0" x:Name="bidDepthList" ItemsSource="{Binding Source={StaticResource BidDepthCollection}}"
Visibility="{Binding Path=IsMultilegEnabled, Converter={StaticResource CollapsedConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewmodels:DepthLevelViewModel}">
<v:DepthLevelRowView x:Name="BidDepthLevelRowViewControl" DataContext="{Binding}" HorizontalAlignment="Center" Margin="1,0,1,3" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- ASK (BUY) DEPTH -->
<ItemsControl Grid.Row="0" Grid.Column="1" x:Name="askDepthList" ItemsSource="{Binding Source={StaticResource AskDepthCollection}}"
Visibility="{Binding Path=IsMultilegEnabled, Converter={StaticResource CollapsedConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewmodels:DepthLevelViewModel}">
<v:DepthLevelRowView x:Name="AskDepthLevelRowViewControl" DataContext="{Binding}" HorizontalAlignment="Center" Margin="1,0,1,3" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The view being used has 4 "controls" inside of a grid. Only one of them is displayed at a time depending on the state (RFS OFF/RFS ON) and which side they are on (Sell/Buy). The others are collapsed. As you can see, this is fine.
The only common factor between them is that they have their Command set, as do most of the controls at the top that are disabling/enabling correctly. The fact that the buttons enable correctly if any mouse or keyboard action is taken tells me that the CanExecute handler is working, just not immediately. The other controls started working after I made these changes, but then the big buttons started misbehaving like the depth buttons have been doing.
I've tried using CommandManager.InvalidateRequerySuggested(); after altering the collections, but that didn't help.
NOTE: This is also happening even for something as simple as this:
<Button x:Name="TestBuyButton" Command="{x:Static ptcommands:OrderCommands.Buy}" CommandTarget="{Binding RelativeSource={RelativeSource Self}}" Content="Test Buy" />
I know the booleans are set correctly because I added test CheckBoxes to display the current value with IsChecked. And, they all enable, including the extremely basic Button, as soon as any input action is taken.
Is there something else I'm missing or a different approach I can take?
EDIT: I ended up using the Dispatcher to invoke my display update routine from the event thread over to the UI thread. The boolean values get set, but WPF still didn't requery the Command. BUT.. the CommandManager.InvalidateRequerySuggested() call then worked! The only thing I don't like about that is it sounds like a broadcast invalidation. I don't want all commands to be requeried. I just want the Buy and Sell commands to requery. I've tried all sorts of weirdness trying to get just those to work, but nothing has worked so far other than the global.
EDIT 2: It appears as though the InvalidateRequerySuggested is the way to go.
From MSDN CommandManager.InvalidateRequerySuggested:
The CommandManager only pays attention to certain conditions in determining when the command target has changed, such as change in keyboard focus. In situations where the CommandManager does not sufficiently determine a change in conditions that cause a command to not be able to execute, InvalidateRequerySuggested can be called to force the CommandManager to raise the RequerySuggested event.
This would explain why the focus changes and keypresses would cause the buttons to enable. The page also shows putting the call in a timer. Because of this, I assume it is not as resource intensive as I thought. So, I guess it's my permanent solution.
I had to make a few changes to get the buttons to enable without focus changes.
The boolean values were being set within a method called from an event thread. I had tried calling CommandManager.InvalidateRequerySuggested along with it, but nothing happened. Eventually I thought it might have something to do with the thread. This is what ended up resolving it.
In the parent form that the tab manager class (containing the event handlers and other logic) has access to, I added an invoke method:
Public Sub InvokeOnUiThread(ByRef uiAction As Action, Optional ByRef doAsync As Boolean = False)
Dim dispatchObject As Dispatcher = orderTicketView.Dispatcher
If (dispatchObject Is Nothing OrElse dispatchObject.CheckAccess()) Then
uiAction()
Else
If doAsync Then
dispatchObject.BeginInvoke(uiAction, DispatcherPriority.Normal)
Else
dispatchObject.Invoke(uiAction, DispatcherPriority.Normal)
End If
End If
End Sub
In the tab manager event handler, I changed it to call the update routine through the invoker:
formOrderTicketView.InvokeOnUiThread(New Action(AddressOf UpdateButtons))
At the bottom of the UpdateButtons method, I added a call to the CommandManager if a change has been made that would require a requery:
CommandManager.InvalidateRequerySuggested()
I do not know what type of performance hit this would have, but apparently WPF executes it in its own way when focus changes and such. Calling it directly is the advised way to force it.
From MSDN CommandManager.InvalidateRequerySuggested:
The CommandManager only pays attention to certain conditions in determining when the command target has changed, such as change in keyboard focus. In situations where the CommandManager does not sufficiently determine a change in conditions that cause a command to not be able to execute, InvalidateRequerySuggested can be called to force the CommandManager to raise the RequerySuggested event.
Since it is working now, I am taking this as the resolution.
The command that's bound to the button...is there a CanExecute delegate with it? If so, you have to raise CanExecuteChanged when that delegate should be re-evaluated. Also, make sure the implementation of CanExecute isn't broken and incorrectly returning false;
Another strange behavior in my MVVM-Adventures I can't explain and didn't find a reasoning:
I have a Command in my VM, which can be executed, when a Color is selected:
public ICommand SetSingleColor
{
get
{
return new RelayCommand(f =>
{
var visualLed = f as VisualLed;
visualLed.Color = SelectedColor.Value;
}, () => SelectedColor.HasValue);
}
}
My DataTemplate is looking like this:
<DataTemplate DataType="{x:Type ml:VisualLed}" x:Key="DtVisualLed">
<Button
Background="DarkCyan"
Command="{Binding Path=DataContext.SetSingleColor, RelativeSource={RelativeSource AncestorType=v:LedDesignView}}"
CommandParameter="{Binding}"
Style="{StaticResource StyleBtnLed}">
</Button>
</DataTemplate>
As u can see, I pass the VisualLed itself back to the VM, so I can set the Color. I just added the Background-Property for testing purposes.
What I really don't get: The Style and also the Background are only applied, if the Command can be executed! So if I load the View, the Button-Background is the Default-Gray, and as soon as I select a Color, it becomes DarkCyan.
Unfortunately, I can't give more Informations, but I didn't find anything on the Topic, how ButtonBase.Command influences other Properties. It also seems like a expected behavior, since I don't get any Binding-Errors or such.
There is relation between Button.Command and Button.IsEnabled property. If Command.CanExecute returns false, button becomes disabled. Now, you often cannot control everything with your own style, if control developer did not plan for this. In this case, disabled button background is fixed by developers of the Button and you cannot override it with your own style (unless you change Template of the button).
I'm having trouble getting the RelayCommand to enable/disable the attached control properly.
I've got an EventToCommand element attached to a button. The command is databound to the ViewModel. Initially, the button is disabled (expected behavior), but I cannot seem to get the CanExecute logic to check it's value. When CurrentConfigFile is set and exists, the button should be enabled. I've executed code and checked the file's value in debug to make sure it's set, but the control is still disabled. I've tried CommandManager.InvalidateRequerySuggested() and command.RaiseCanExecuteChanged(), but it will not enable.
I've wondered if lambdas don't work correctly for the CanExecute behavior (even though the examples use them) or that the CanExecute behavior needs to be databound to another element.
Here's my code:
// The FileInfo being checked for existence before the button should be enabled
public const string CurrentConfigFilePN = "CurrentConfigFile";
public FileInfo CurrentConfigFile
{
get
{
return _currentConfigFile;
}
set
{
if (_currentConfigFile == value)
{
return;
}
var oldValue = _currentConfigFile;
_currentConfigFile = value;
// Update bindings, no broadcast
RaisePropertyChanged(CurrentConfigFilePN);
}
}
public MainViewModel()
{
// snip //
SaveCommand = new RelayCommand(SaveConfiguration,
() => CurrentConfigFile != null && CurrentConfigFile.Exists);
}
private void SaveConfiguration()
{
// export model information to xml document
ExportXMLConfiguration(CurrentConfigFile);
}
and markup
<Button x:Name="SaveButton" Content="Save" Width="75" Margin="20,5">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<GalaSoft:EventToCommand x:Name="SaveETC"
Command="{Binding SaveCommand}"
MustToggleIsEnabledValue="true" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
Update:
As per Isak Savo's suggestion, I bound the RelayCommand directly to the button with
<Button x:Name="SaveButton" Content="Save" Width="75" Margin="20,5"
Command="{Binding SaveCommand}"/>
and it started disabled and correctly enabled when the FileInfo was set. Guess I should remember not to fix what isn't broken!
Why don't you just bind to the Command directly from the Button?
<Button Command="{Binding SaveCommand}" Content="Save" />
Maybe the EventToCommand thing you are using is messing things up with the command's CanExecute notification.
And regarding the CanExecute problem - are you sure that your CanExecute handler is called after the CurrentConfigFile property is set? I've found that even though WPF mostly does a good job of requerying CanExecute, I still sometimes need to force a requery through the CommandManager.
EDIT: As pointed out in the comments, the OP has already tried the command manager approach.
In msdn is written:
When first called, FileInfo calls Refresh and caches information about the file. On subsequent calls, you must call Refresh to get the latest copy of the information.
However, I would not do such a check in the CanExecute-handler. This may slow down your UI because CanExecute is called a lot of times and I could imagine that such IO-checks can become slow, for example if the file lies on a network share.