CanExecute on RelayCommand<T> not working
Asked Answered
S

3

11

I'm writing a WPF 4 app (with VS2010 RC) using MVVM Light V3 alpha 3 and am running into some weird behaviour here...

I have a command that opens a Window, and that Window creates the ViewModel and so on - nothing weird there.

In that Window I have some RelayCommands, for example:

CategoryBeenSelected = new RelayCommand(() => OnCategoryUpdate = true);

Nothing weird again - it works as I expected.

The problem is that I cannot have a CanExecute method / lambda expression with a generic RelayCommand.

This works:

DeleteCategoryCommand = new RelayCommand<int>(DeleteCategory);

But this does not:

DeleteCategoryCommand = new RelayCommand<int>(DeleteCategory, CanDeleteCategory);

The Window doesn't show up. I mean, I click the button that opens the window, and the app just gets blocked and some seconds later, The Window's InitializeComponent method throws a NullReferenceException (Object reference not set to an instance of an object)

In short, If I put a CanExecute Method on a RelayCommand<T>, the Window that owns that ViewModel (with the RelayCommand<T>) can't be instantiated. If I remove the CanExecute, the Window shows up.

Where is the problem here? I'm confused.

Thank you.

EDIT: As requested, here is the stack trace:

A first chance exception of type 'System.NullReferenceException' occurred in PresentationFramework.dll
   at GalaSoft.MvvmLight.Command.RelayCommand`1.CanExecute(Object parameter)
   at System.Windows.Controls.Primitives.ButtonBase.UpdateCanExecute()
   at System.Windows.Controls.Primitives.ButtonBase.OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
   at System.Windows.DependencyObject.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
   at System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e)
   at System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args)
   at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
   at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal)
   at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value)
   at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetValue(Object inst, XamlMember property, Object value)
   at MS.Internal.Xaml.Runtime.PartialTrustTolerantRuntime.SetValue(Object obj, XamlMember property, Object value)
   at System.Xaml.XamlObjectWriter.Logic_ApplyPropertyValue(ObjectWriterContext ctx, XamlMember prop, Object value, Boolean onParent)
   at System.Xaml.XamlObjectWriter.Logic_DoAssignmentToParentProperty(ObjectWriterContext ctx)
   at System.Xaml.XamlObjectWriter.WriteEndObject()
   at System.Windows.Markup.WpfXamlLoader.TransformNodes(XamlReader xamlReader, XamlObjectWriter xamlWriter, Boolean onlyLoadOneNode, Boolean skipJournaledProperties, Boolean shouldPassLineNumberInfo, IXamlLineInfo xamlLineInfo, IXamlLineInfoConsumer xamlLineInfoConsumer, XamlContextStack`1 stack, IStyleConnector styleConnector)
   at System.Windows.Markup.WpfXamlLoader.Load(XamlReader xamlReader, IXamlObjectWriterFactory writerFactory, Boolean skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri)
   at System.Windows.Markup.WpfXamlLoader.LoadBaml(XamlReader xamlReader, Boolean skipJournaledProperties, Object rootObject, XamlAccessLevel accessLevel, Uri baseUri)
   at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream)
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
   at ApuntaNotas.Views.CategoryEditorView.InitializeComponent() in c:\Users\Jesus\Documents\Visual Studio 2010\Projects\ApuntaNotas\ApuntaNotas\Views\CategoryEditorView.xaml:line 1
   at ApuntaNotas.Views.CategoryEditorView..ctor() in C:\Users\Jesus\Documents\Visual Studio 2010\Projects\ApuntaNotas\ApuntaNotas\Views\CategoryEditorView.xaml.cs:line 18
A first chance exception of type 'System.NullReferenceException' occurred in PresentationFramework.dll
Sikorsky answered 21/2, 2010 at 13:49 Comment(6)
It's strange: Reflector tells that the function CanExecute is defined in such a way: public bool CanExecute(object parameter) { return (this._canExecute == null) || this._canExecute((T) parameter)); }. There is nothing to throw an exception.Ketubim
Maybe you can try to reproduce the problem on a smaller example?Ketubim
Aha, I got something new. It works if you have a RelayCommand with a string or an object, if you use something else (int, bool, double..) it die. It's irrelevant if you actually send a parameter or not. About the example. It happens in WPF3.5 / Mvvm light 2 and WPF4 / MVVM light 3 alpha3. What can you run? (I don't know if you need MVVM light installer or not)Sikorsky
I'll be able to run at work, only tomorrow. WPF3.5 would be fine.Ketubim
I found the bug, It is inside RelayCommand<T> class. I will send an email to the class creator.Sikorsky
Maybe, at this time, parameter is null?Louralourdes
C
7

It seems that the RelayCommand will cast the value the parameter to the generic T.

But you cannot cast a null to a struct, as the exception tells you!

If you initialize the RelayCommand with a nullable struct, it will work as expected!

RelayCommand<int?> or RelayCommand<Nullable<int>>

HTH

Canter answered 23/2, 2010 at 16:34 Comment(3)
Uhm, that should be the reason... But it is a little weird.. I didn't see any code using nullable ...Sikorsky
Yes that is correct.. a double or int are value types, and cannot be null. If you make them nullable types, it should work. Casting null to a struct will produce an exception! See Vlads comment with the method where you can see the cast to T!Canter
try compiling double test = (double)null;.. in the generic world you will get a runtime exception! ;)Canter
D
2

Arcturus was correct in identifying what the problem was, however I didn't like the solution of using nullable primitives. I personally don't like nullable primitives unless I have a very good reason to use them.

Instead, I changed the implementation of RelayCommand as follows:

    bool ICommand.CanExecute(object parameter)
    {
        if (parameter == null && typeof(T).IsValueType)
        {
            return CanExecute(default(T));
        }
        return CanExecute((T)parameter);
    }

I didn't make this same change for the generic Execute method (at least for now) because I don't think it is unreasonable to fail in that case if the command really does expect an argument.

The problem with CanExecute is that the WPF system will sometimes call it before certain bindings can be evaluated. For example:

        <Button Content="Fit To Width" Command="{Binding Path=FitToWidthCommand}" CommandParameter="{Binding ElementName=imageScrollViewer, Path=ActualWidth}" />
        <Button Content="Fit To Height" Command="{Binding Path=FitToHeightCommand}" CommandParameter="{Binding ElementName=imageScrollViewer, Path=ActualHeight}" />

In the above XAML, you notice the command parameter is bound to the actual width of a control. However, WPF will call CanExecute on the button's command before the "imageScrollViewer" control is necessarily laid out/rendered - so there is no actual width/height. By the time the user clicks the button and Execute is invoked, of course the control is laid out so values get sent to the command. If not - I think failing is what should be expected - but only when the user actually clicks the button.

Of course I don't like the different behavior of CanExecute and Execute, but for now it seems to fit within the restrictions presented by the framework. I may find a scenario where this causes me grief, but I've been liking the change so far.

Dincolo answered 23/2, 2011 at 18:41 Comment(0)
P
1

Incredibly late to the party but I've just been scratching my head over this and the issue was I'd imported the wrong namespace.

I should have imported:

using GalaSoft.MvvmLight.CommandWpf;

but I had imported:

using GalaSoft.MvvmLight.Command;.

Hope this helps someone!

Paraphernalia answered 8/10, 2019 at 13:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.