Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CanExecute on RelayCommand<T> not working

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
like image 383
Jesus Rodriguez Avatar asked Feb 21 '10 13:02

Jesus Rodriguez


2 Answers

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

like image 133
Arcturus Avatar answered Oct 17 '22 17:10

Arcturus


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.

like image 20
Bojingo Avatar answered Oct 17 '22 15:10

Bojingo