RelayCommands and WeakFuncs

January 16, 2015

2 comments

This week I was deep inside refactoring a huge very non-MVVM-y graphics codebase we have. This codebase is not a standalone tool but rather a library of WPF UserControls and graphics capabilities which is used in several tools in the department – both for internal use and in the HMI, which we finally give to our customers (I remind you that I work for BrightSoure Energy and we build solar power plants! How cool is that, huh?). This library was written a long time ago when we were just getting into C# and WPF and had tight deadlines. Moreover, it was specifically tailored for the particular project we were working. But today we’re a lot smarter and we have more projects and the library does not allow for much customization and is not very convenient to work with. So whenever a new feature is required we try to write it in a more extensible and MVVM-y style which sometimes (well, most of the times) also requires refactoring that good old code (which, by the way, works pretty well, so kudos to the beginner C#-ers who made that happen).

Anyway, this week I had to add a context menu to a control we have which uses a DataGrid. That in itself isn’t very hard but I took into account that although we have a predefined set of operations which can be applied on the elements of the grid, it might be very helpful if my users could also add their own commands to the party; especially since the control is rather generic and can show data that applies to different objects.

Here’s my general design: The UserControl has a “ViewModel” which holds the relevant data for the display – the data for the grid and the list of commands which will be in the context menu. I put “ViewModel” in quotes because it’s a reusable UserControl, so it can’t have a view model (which I am aware of as a library author). Instead the control has a DependencyProperty which serves all the required data and that in turn can be supplied by my users from a real view model. The data for the grid will be an ObservableCollection of the data to display and the list of commands will also be an ObservableCollection of a logical command data structure. The data will be bound to the ItemsSource of the grid and the commands will be bound to the ItemsSource of the ContextMenu (turns out you can do that!).

My problem was that some of the commands I added worked perfectly and some didn’t. Those that didn’t work weren’t even invoked (debugger wouldn’t stop inside the command code).

Lets us start recreating. I’ll use a sample project I concocted for this purpose – a simple UI which allows users to log in the system, view their list of friends and perform some operations on these friends:

friendsApp

(Side note: taking a screen shot of an application with a context menu open in a Windows remote desktop from Mac was the hardest thing I ever had to do…)

This application is written using MVVM facilitated by MVVMLight. It has a ViewModelLocator class which is a Resource defined in the App.xaml. It provides three view models: MainViewModel, LoginViewModel and FriendsViewModel. The data flows between these view models using services which provide data and raise events about interesting stuff: IUserSerivce and IUserDataService. IUserDataService represents data that comes from a repository about users and their friends:

interface IUserDataService
{
    UserData GetData(string name);

    IEnumerable<UserData> GetFriends(string name);
}

And IUserService is used to pass data around in the application (I admit the choice of names is not the best):

interface IUserService
{
    void SetUser(UserData user);

    UserData GetCurrentUser();

    event EventHandler<UserChangedEventArgs> UserChanged;
}

The view models obtain these services in the constructors and can use them as required. For instance, the LoginViewModel implements the LoginCommand using both services:

private ICommand _loginCommand;
public ICommand LoginCommand
{
    get
    {
        return _loginCommand ?? (_loginCommand = new RelayCommand<string>(user =>
            {
                try 
                { 
                    _userService.SetUser(_userDataService.GetData(user));
                }
                catch(Exception e)
                {
                    _userMessageService.DisplayMessage("Error occurred",
                        "Could not Login with the specified user. Make sure the credentials are correct.");
                }
            }));
    }
}

And the FriendsViewModel registers for the UserChanged event to update the friends list:

public FriendsViewModel(IUserService userService, IUserDataService userDataService)
{
    userService.UserChanged += (s, e) =>
        {
            Friends.Clear();
            if (e.NewUser != null)
            {
                foreach (var f in userDataService.GetFriends(e.NewUser.UserName))
                {
                    Friends.Add(f);
                }
            }

        };

    // more stuff to be seen later
}

Now we get to a slightly more interesting part. I wanted to simulate the situation I had at work where a control allowed the user to add commands from the outside. So here’s my model command class:

class FriendCommand : ObservableObject
{
    public string Header { get; set; }
    public ICommand Command { get; set; }
}

And FriendsViewModel holds a collection of them:

private ObservableCollection<FriendCommand> _commands = new ObservableCollection<FriendCommand>();
public ObservableCollection<FriendCommand> Commands
{
    get { return _commands; }
}

The “Remove” option is inherent to the FriendsViewModel so it’s added right in the constructor:

public FriendsViewModel(IUserService userService, IUserDataService userDataService)
{
    userService.UserChanged += (s, e) =>
        {
            Friends.Clear();
            if (e.NewUser != null)
            {
                foreach (var f in userDataService.GetFriends(e.NewUser.UserName))
                {
                    Friends.Add(f);
                }
            }

        };

    Commands.Add(new FriendCommand
        {
            Header = "Remove",
            Command = new RelayCommand<UserData>(user =>
            {
                // userDataService.remove(currentUser, user);
                Friends.Remove(user);
            })
        });
}

And the “Login with this user” option is added from the outside – from the MainViewModel (just like my users can add commands to the DataGrid’s ContextMenu).

public MainViewModel(IUserService userService,
    IUserMessageService userMessageService,
    FriendsViewModel friendsViewModel)
{
    Title = "Relay Commands Demo";

    userService.UserChanged += (s, e) =>
        {
            CurrentUser = e.NewUser;
        };

    friendsViewModel.Commands.Add(new FriendCommand
        {
            Header = "Login with this user",
            Command = new RelayCommand<UserData>(user=>
            {
                userService.SetUser(user);
            })
        });
}

And this is how the FriendsControl is wired to use these commands:

<UserControl x:Class="RelayCommands.Views.FriendsControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:views="clr-namespace:RelayCommands.Views"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             DataContext="{Binding Source={StaticResource ViewModelLocator}, Path=FriendsViewModel}">
    <StackPanel>
        <TextBlock>Friends:</TextBlock>
        <ListBox ItemsSource="{Binding Friends}" SelectionMode="Single" SelectedItem="{Binding CurrentFriend}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <views:UserDataControl></views:UserDataControl>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ContextMenu>
                <ContextMenu ItemsSource="{Binding Source={StaticResource ViewModelLocator},Path=FriendsViewModel.Commands}">
                    <ContextMenu.ItemContainerStyle>
                        <Style TargetType="MenuItem">
                            <Setter Property="Header" Value="{Binding Header}"></Setter>
                            <Setter Property="Command" Value="{Binding Command}"></Setter>
                            <Setter Property="CommandParameter" 
                                    Value="{Binding Source={StaticResource ViewModelLocator}, Path=FriendsViewModel.CurrentFriend}">
                            </Setter>
                         </Style>
                    </ContextMenu.ItemContainerStyle>
                </ContextMenu>
            </ListBox.ContextMenu>
        </ListBox>
    </StackPanel>
</UserControl>

And this is where the bug happens. The binding to the Commands collection obviously works – you saw in the screenshot that we see both commands. And although I can’t show you a screenshot of this, you will just have to believe me – the Remove command works and the Login doesn’t*. Well, this is kind of weird. How can the location of initialization affect the execution/binding of the command??

The first thing to do when something doesn’t work in WPF is run the application with the debugger attached and look in the Visual Studio Output window – it might display binding errors or other exceptions that happened in your application and WPF just sort of swallowed them. So, I ran the application with the debugger attached. No exceptions, no binding errors, nothing.

When that happens it’s time for the big guns – WPF binding debugging. Although it’s very easy, I never remember the exact syntax. God bless the Internet. The command isn’t invoked so I attach the binding diagnosis to the Command binding:

<Setter Property="Command" Value="{Binding Command, diag:PresentationTraceSources.TraceLevel=High}"></Setter>

Here’s what I got:

System.Windows.Data Warning: 58 :   Path: 'Command'
System.Windows.Data Warning: 60 : BindingExpression (hash=58915139): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=58915139): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=58915139): Attach to System.Windows.Controls.MenuItem.Command (hash=4181390)
System.Windows.Data Warning: 67 : BindingExpression (hash=58915139): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=58915139): Found data context element: MenuItem (hash=4181390) (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=58915139): Activate with root item FriendCommand (hash=21106248)
System.Windows.Data Warning: 108 : BindingExpression (hash=58915139):   At level 0 - for FriendCommand.Command found accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 104 : BindingExpression (hash=58915139): Replace item at level 0 with FriendCommand (hash=21106248), using accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 101 : BindingExpression (hash=58915139): GetValue at level 0 from FriendCommand (hash=21106248) using RuntimePropertyInfo(Command): RelayCommand`1 (hash=11599658)
System.Windows.Data Warning: 80 : BindingExpression (hash=58915139): TransferValue - got raw value RelayCommand`1 (hash=11599658)
System.Windows.Data Warning: 89 : BindingExpression (hash=58915139): TransferValue - using final value RelayCommand`1 (hash=11599658)
System.Windows.Data Warning: 56 : Created BindingExpression (hash=43687586) for Binding (hash=4249251)
System.Windows.Data Warning: 58 :   Path: 'Command'
System.Windows.Data Warning: 60 : BindingExpression (hash=43687586): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=43687586): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=43687586): Attach to System.Windows.Controls.MenuItem.Command (hash=19784333)
System.Windows.Data Warning: 67 : BindingExpression (hash=43687586): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=43687586): Found data context element: MenuItem (hash=19784333) (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=43687586): Activate with root item FriendCommand (hash=35945528)
System.Windows.Data Warning: 107 : BindingExpression (hash=43687586):   At level 0 using cached accessor for FriendCommand.Command: RuntimePropertyInfo(Command)
System.Windows.Data Warning: 104 : BindingExpression (hash=43687586): Replace item at level 0 with FriendCommand (hash=35945528), using accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 101 : BindingExpression (hash=43687586): GetValue at level 0 from FriendCommand (hash=35945528) using RuntimePropertyInfo(Command): RelayCommand`1 (hash=66392805)
System.Windows.Data Warning: 80 : BindingExpression (hash=43687586): TransferValue - got raw value RelayCommand`1 (hash=66392805)
System.Windows.Data Warning: 89 : BindingExpression (hash=43687586): TransferValue - using final value RelayCommand`1 (hash=66392805)

Let’s analyze this: we can see traces of two binding operations – both for the Command property. That makes sense, there are two commands in the context menu. I must admit I don’t understand fully each line of the trace but it’s pretty obvious that both operations end up using concrete non-null RelayCommand instances, they have hash codes and everything. When a binding doesn’t work it’s usually pretty easy to understand it from the trace – it says stuff like “could not find property this and that on source that or other” or “something went wrong with conversion, using null instead” – pretty clear error messages. This trace looks absolutely fine.

Alright then, maybe it somehow ended up using the wrong RelayCommands? I don’t know how to look exactly what’s inside the Command WPF ended up using, but I can compare hash codes. So I ran the debugger again. This time I added two breakpoints – one after each RelayCommand<UserData> initialization:

Here (in FriendsViewModel):

Commands.Add(new FriendCommand
{
    Header = "Remove",
    Command = new RelayCommand<UserData>(user =>
    {
        // userDataService.remove(currentUser, user);
        Friends.Remove(user);
    })
});

and here (in MainViewModel):

friendsViewModel.Commands.Add(new FriendCommand
{
    Header = "Login with this user",
    Command = new RelayCommand<UserData>(user=>
    {
        userService.SetUser(user);
    })
});

After the initialization we can use the Immediate Window to get the hash codes of the initialized RelayCommands:

image

Now let’s look at the binding trace again:

System.Windows.Data Warning: 56 : Created BindingExpression (hash=17211952) for Binding (hash=33664274)
System.Windows.Data Warning: 58 :   Path: 'Command'
System.Windows.Data Warning: 60 : BindingExpression (hash=17211952): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=17211952): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=17211952): Attach to System.Windows.Controls.MenuItem.Command (hash=51617741)
System.Windows.Data Warning: 67 : BindingExpression (hash=17211952): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=17211952): Found data context element: MenuItem (hash=51617741) (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=17211952): Activate with root item FriendCommand (hash=62887372)
System.Windows.Data Warning: 108 : BindingExpression (hash=17211952):   At level 0 - for FriendCommand.Command found accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 104 : BindingExpression (hash=17211952): Replace item at level 0 with FriendCommand (hash=62887372), using accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 101 : BindingExpression (hash=17211952): GetValue at level 0 from FriendCommand (hash=62887372) using RuntimePropertyInfo(Command): RelayCommand`1 (hash=50429688)
System.Windows.Data Warning: 80 : BindingExpression (hash=17211952): TransferValue - got raw value RelayCommand`1 (hash=50429688)
System.Windows.Data Warning: 89 : BindingExpression (hash=17211952): TransferValue - using final value RelayCommand`1 (hash=50429688)
System.Windows.Data Warning: 56 : Created BindingExpression (hash=2849651) for Binding (hash=33664274)
System.Windows.Data Warning: 58 :   Path: 'Command'
System.Windows.Data Warning: 60 : BindingExpression (hash=2849651): Default mode resolved to OneWay
System.Windows.Data Warning: 61 : BindingExpression (hash=2849651): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=2849651): Attach to System.Windows.Controls.MenuItem.Command (hash=49726848)
System.Windows.Data Warning: 67 : BindingExpression (hash=2849651): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=2849651): Found data context element: MenuItem (hash=49726848) (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=2849651): Activate with root item FriendCommand (hash=52578955)
System.Windows.Data Warning: 107 : BindingExpression (hash=2849651):   At level 0 using cached accessor for FriendCommand.Command: RuntimePropertyInfo(Command)
System.Windows.Data Warning: 104 : BindingExpression (hash=2849651): Replace item at level 0 with FriendCommand (hash=52578955), using accessor RuntimePropertyInfo(Command)
System.Windows.Data Warning: 101 : BindingExpression (hash=2849651): GetValue at level 0 from FriendCommand (hash=52578955) using RuntimePropertyInfo(Command): RelayCommand`1 (hash=54351321)
System.Windows.Data Warning: 80 : BindingExpression (hash=2849651): TransferValue - got raw value RelayCommand`1 (hash=54351321)
System.Windows.Data Warning: 89 : BindingExpression (hash=2849651): TransferValue - using final value RelayCommand`1 (hash=54351321)

So the binding ended up using the correct commands, in the correct order. I really couldn’t believe my eyes and got a bit paranoid for a moment. Is it possible for different objects to have the same hash code but be actually different? In theory it’s possible if you override the GetHashCode method (or if the hashcode ended up being reused by two different instances, which for reference types is extremely unlikely unless you create billions of objects). Thank God for ILSpy.

image

No override for GetHashCode. So the Commands absolutely and utterly must be correct!!! At this point I started getting a feeling that the commands must have somehow been garbage-collected after initialization. But on the other hand I know for sure that trying to run a collected method causes an exception and none were reported by the Output window. Went back to good old Internet friend. After some searching, came across this StackOverflow question. It wasn’t exactly my situation, since I didn’t get any exceptions, but it got me thinking – if the code works with a different RelayCommand implementation the answer must lie in MVVMLight’s implementation. How is it different from an implementation which supposedly works? This time I decided to use the actual sources for MVVMLight instead of relying on ILSpy. Here are the parts that matter:

// ****************************************************************************
// <copyright file="RelayCommandGeneric.cs" company="GalaSoft Laurent Bugnion">
// Copyright © GalaSoft Laurent Bugnion 2009-2014
// </copyright>
// ****************************************************************************
// <author>Laurent Bugnion</author>
// <email>laurent@galasoft.ch</email>
// <date>22.4.2009</date>
// <project>GalaSoft.MvvmLight</project>
// <web>http://www.mvvmlight.net</web>
// <license>
// See license.txt in this project or http://www.galasoft.ch/license_MIT.txt
// </license>
// ****************************************************************************
// <credits>This class was developed by Josh Smith (http://joshsmithonwpf.wordpress.com) and
// slightly modified with his permission.</credits>
// ****************************************************************************

using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Input;
using GalaSoft.MvvmLight.Helpers;

namespace GalaSoft.MvvmLight.CommandWpf
{
    public class RelayCommand<T> : ICommand
    {
        private readonly WeakAction<T> _execute;

        private readonly WeakFunc<T, bool> _canExecute;

        public RelayCommand(Action<T> execute)
            : this(execute, null)
        {
        }

        public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }

            _execute = new WeakAction<T>(execute);

            if (canExecute != null)
            {
                _canExecute = new WeakFunc<T,bool>(canExecute);
            }
        }

        public virtual void Execute(object parameter)
        {
            var val = parameter;

            if (CanExecute(val)
                && _execute != null
                && (_execute.IsStatic || _execute.IsAlive))
            {
                // execute _execute on val
            }
        }
    }
}

First, let’s look at the Execution method – it checks a bunch of conditions – conditions which allow safe execution of the code – and if they don’t apply, it silently does nothing. Absolutely nothing. One of the conditions is the method being alive. Indeed, in order to avoid memory leaks, MVVMLight saves the delegate in a WeakAction class – a MVVMLight wrapper class which saves a WeakReference to the delegate’s target. Such a weak reference does not prevent the delegate’s target and thus the delegate from being collected by the garbage collector. Saving the delegate explicitly saved the problem, even if I explicitly run GC before creating and/or using the menu.

class MainViewModel : ViewModelBase
{
    Action<UserData> _keeperOfTheAction;

    public MainViewModel(IUserService userService,
        IUserMessageService userMessageService,
        FriendsViewModel friendsViewModel)
    {
        Title = "Relay Commands Demo";

        userService.UserChanged += (s, e) =>
            {
                CurrentUser = e.NewUser;
            };

        _keeperOfTheAction = new Action<UserData>(user =>
        {
            userService.SetUser(user);
        });
        friendsViewModel.Commands.Add(new FriendCommand
            {
                Header = "Login with this user",
                Command = new RelayCommand<UserData>(_keeperOfTheAction)
            });

        GCCommand = new RelayCommand(() =>
        {
            GC.Collect();
            userMessageService.DisplayMessage("Operation Complete", "Ran GC successfully");
        });
    }

    // more...
}

Problem solved, right? Well, sort of. Indeed technically the problem was solved. First, I did not like the solution – am I really to keep references to all the inner delegates?? That’s damn ugly!! And second, I use lambda functions to define commands all the time, specifically in this project the Remove command is defined using a lambda function and it works perfectly every single time! I don’t like voodoo behavior and/or solutions. I think I already mentioned this some time before – it makes me feel uncomfortable. I like the coziness of determinism and full transparency.

Understanding this behavior required some more thorough digging. Let’s take a look at the generated IL (again, using ILSpy). This is how it begins:

.class private auto ansi beforefieldinit RelayCommands.ViewModels.MainViewModel
    extends [GalaSoft.MvvmLight]GalaSoft.MvvmLight.ViewModelBase
{
    // Nested Types
    .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass7'
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Fields
        .field public class RelayCommands.ViewModels.MainViewModel '<>4__this'
        .field public class RelayCommands.Services.IUserService userService
        .field public class RelayCommands.Services.IUserMessageService userMessageService

        // Methods
        .method public hidebysig specialname rtspecialname 
            instance void .ctor () cil managed 
        {
            // Method begins at RVA 0x27b8
            // Code size 7 (0x7)
            .maxstack 8

            IL_0000: ldarg.0
            IL_0001: call instance void [mscorlib]System.Object::.ctor()
            IL_0006: ret
        } // end of method '<>c__DisplayClass7'::.ctor

        .method public hidebysig 
            instance void '<.ctor>b__2' (
                class RelayCommands.Models.UserData user
            ) cil managed 
        {
            // Method begins at RVA 0x27d0
            // Code size 15 (0xf)
            .maxstack 8

            IL_0000: nop
            IL_0001: ldarg.0
            IL_0002: ldfld class RelayCommands.Services.IUserService RelayCommands.ViewModels.MainViewModel/'<>c__DisplayClass7'::userService
            IL_0007: ldarg.1
            IL_0008: callvirt instance void RelayCommands.Services.IUserService::SetUser(class RelayCommands.Models.UserData)
            IL_000d: nop
            IL_000e: ret
        } // end of method '<>c__DisplayClass7'::'<.ctor>b__2'

        .method public hidebysig 
            instance void '<.ctor>b__3' () cil managed 
        {
            // Method begins at RVA 0x27e0
            // Code size 30 (0x1e)
            .maxstack 8

            IL_0000: nop
            IL_0001: call void [mscorlib]System.GC::Collect()
            IL_0006: nop
            IL_0007: ldarg.0
            IL_0008: ldfld class RelayCommands.Services.IUserMessageService RelayCommands.ViewModels.MainViewModel/'<>c__DisplayClass7'::userMessageService
            IL_000d: ldstr "Operation Complete"
            IL_0012: ldstr "Ran GC successfully"
            IL_0017: callvirt instance void RelayCommands.Services.IUserMessageService::DisplayMessage(string, string)
            IL_001c: nop
            IL_001d: ret
        } // end of method '<>c__DisplayClass7'::'<.ctor>b__3'

    } // end of class <>c__DisplayClass7
    // more stuff
}

The first thing we see is a definition of a nested class – <>c__DisplayClass7. This is the class that the compiler generates for us as the implementation of the lambda anonymous function. It has all the captured values as fields and a method which is the code of the lambda. But this nested class has two methods! One which gets UserData as input and calls SetUser and one which has no input and calls GC.Collect. Come to think of this, it actually makes sense. We have many lambda functions in this MainViewModel class, but only a single nested class for implementing them. The compiler chose to put more than a single method in the display class. Seems reasonable. But where are the rest of the lambdas?? Let’s look at the rest of the generated IL:

.class private auto ansi beforefieldinit RelayCommands.ViewModels.MainViewModel
    extends [GalaSoft.MvvmLight]GalaSoft.MvvmLight.ViewModelBase
{
    // Nested Types and Fields
    // ...

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            class RelayCommands.Services.IUserService userService,
            class RelayCommands.Services.IUserMessageService userMessageService,
            class RelayCommands.ViewModels.FriendsViewModel friendsViewModel
        ) cil managed 
    {
        // ...
    } // end of method MainViewModel::.ctor

    .method public hidebysig specialname 
        instance string get_Title () cil managed 
    {
        // ... 
    } // end of method MainViewModel::get_Title

    .method public hidebysig specialname 
        instance void set_Title (
            string 'value'
        ) cil managed 
    {
        // ...
    } // end of method MainViewModel::set_Title

    .method public hidebysig specialname 
        instance class RelayCommands.Models.UserData get_CurrentUser () cil managed 
    {
        // ...
    } // end of method MainViewModel::get_CurrentUser

    .method public hidebysig specialname 
        instance void set_CurrentUser (
            class RelayCommands.Models.UserData 'value'
        ) cil managed 
    {
        // ...
    } // end of method MainViewModel::set_CurrentUser

    .method public hidebysig specialname 
        instance class [System]System.Windows.Input.ICommand get_GCCommand () cil managed 
    {
        // ...
    } // end of method MainViewModel::get_GCCommand

    .method private hidebysig specialname 
        instance void set_GCCommand (
            class [System]System.Windows.Input.ICommand 'value'
        ) cil managed 
    {
        // ...
    } // end of method MainViewModel::set_GCCommand

    .method private hidebysig 
        instance void '<.ctor>b__1' (
            object s,
            class RelayCommands.Services.UserChangedEventArgs e
        ) cil managed 
    {
        // ...
    } // end of method MainViewModel::'<.ctor>b__1'

    // properties

} // end of class RelayCommands.ViewModels.MainViewModel

Just by looking at the names and input parameter types one method stands out – void ‘<.ctor>b__1’ ( object s, class RelayCommands.Services.UserChangedEventArgs e ).  This is the lambda we used to register to the UserChanged event!!! Why is this lambda not part of the display class? Well, it turns out that when the compiler notices that the lambda doesn’t need to capture any variables, and only uses instance methods/properties/fields then it just adds the lambda as another instance method for the class. Again – a reasonable implementation detail. But this implementation detail has huge consequences: such a lambda will be alive as long as the class is alive!

Now let’s look at the code which generates the Remove command:

Commands.Add(new FriendCommand
{
    Header = "Remove",
    Command = new RelayCommand<UserData>(user =>
    {
        // userDataService.remove(currentUser, user);
        Friends.Remove(user);
    })
});

The lambda doesn’t capture anything. Friends is an instance property of the class FriendsViewModel and user is the input of the lambda. And indeed, this lambda is simply added by the compiler as another method of the class:

.class private auto ansi beforefieldinit RelayCommands.ViewModels.FriendsViewModel
    extends [GalaSoft.MvvmLight]GalaSoft.MvvmLight.ViewModelBase
{
    // Nested Types, fields, other methods, etc.
    // ...

    .method private hidebysig 
        instance void '<.ctor>b__2' (
            class RelayCommands.Models.UserData user
        ) cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x254c
        // Code size 15 (0xf)
        .maxstack 8

        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: call instance class [System]System.Collections.ObjectModel.ObservableCollection`1<class RelayCommands.Models.UserData> RelayCommands.ViewModels.FriendsViewModel::get_Friends()
        IL_0007: ldarg.1
        IL_0008: callvirt instance bool class [mscorlib]System.Collections.ObjectModel.Collection`1<class RelayCommands.Models.UserData>::Remove(!0)
        IL_000d: pop
        IL_000e: ret
    } // end of method FriendsViewModel::'<.ctor>b__2'

    // Properties
    // ...

} // end of class RelayCommands.ViewModels.FriendsViewModel

So this lambda stays alive as long as the FriendsViewModel instance is alive – which is all through the runtime of our application! But the Login command, which is represented by a helper nested class simply gets collected as nothing is keeping this class alive…

That’s it. Mystery solved! Since the thing with adding lambda functions that don’t capture as instance methods is an implementation detail, I guess that the correct way to go about this is always register the RelayCommands with named methods of the view model class.

If you want to read a live depiction of the debugging process, you can go to my StackOverflow question.

* Well, this is partial a lie. It was true for the original bug I had a work, but if you download the code from my blogsamples repository, you will see that at the beginning everything is OK and after a while the Login command stops working.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

2 comments

  1. Kevin GosseJanuary 19, 2015 ב 10:34 PM

    Found this blog thanks to a retweet of Sasha Goldshtein. It looks really promising, please keep up the good work!

    Reply
  2. XavierJune 10, 2016 ב 6:37 AM

    Thank you for posting this! I had the exact same problem.

    Reply