Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do I have to capture the lambda to a field variable in call to constructor

I was recently bit by a weird thing with lambda expression and variable captures. The code was a WPF/MVVM application using .NET 4.5 (VS2012). I was using different constructors of my viewmodel to setup the callback for a RelayCommand (this command would then be bound to a menu item in my view)

In essence, I had the following code:

public class MyViewModel : ViewModelBase
{
    public MyViewModel(Action menuCallback)
    {
        MyCommand = new RelayCommand(menuCallback);
    }

    public MyViewModel(Func<ViewModelBase> viewModelCreator)
    // I also tried calling the other constructor, but the result was the same
    //  : this(() => SetMainContent(viewModelCreator())
    {
        Action action = () => SetMainContent(viewModelCreator());
        MyCommand = new RelayCommand(action);
    }

    public ICommand MyCommand { get; private set; }
}

and then created instances of the above using:

// From some other viewmodel's code:
new MyViewModel(() => new SomeViewModel());
new MyViewModel(() => new SomeOtherViewModel());

These were then bound to a WPF Menu - each menu item had a MyViewModel instance as its data context . The weird thing was that the menus only worked once. Regardless of which of the items I tried, it would call the appropriate Func<ViewModelBase> - but only one time. If I tried to select another menu item or even the same item again, it simply didn't work. Nothing got called and no output in the VS debug output about any errors.

I'm aware of issues with variable captures in loops, so I made a guess that this issue was related so changed my VM to:

public class MyViewModel : ViewModelBase
{
    public MyViewModel(Action buttonCallback)
    {
        MyCommand = new RelayCommand(buttonCallback);
    }
    private Func<ViewModelBase> _creator;
    public MyViewModel(Func<ViewModelBase> viewModelCreator)
    {
        // Store the Func<> to a field and use that in the Action lambda
        _creator = viewModelCreator;
        var action = () => SetMainContent(_creator());
        MyCommand = new RelayCommand(action);
    }

    public ICommand MyCommand { get; private set; }
}

and called it the same way. Now everything works as it should.

Just for fun, I also worked around the whole Func<ViewModelBase> constructor by creating the appropriate Action outside of the MyViewModel constructor:

// This code also works, even without the _creator field in MyViewModel
new MyViewModel(() => SetMainContent(new SomeViewModel()));
new MyViewModel(() => SetMainContent(new SomeOtherViewModel()));

So I managed to get it working, but I'm still curious why it works like this. Why doesn't the compiler properly capture the Func<ViewModelBase> in the constructor?

like image 204
Isak Savo Avatar asked Oct 04 '12 12:10

Isak Savo


1 Answers

I'm guessing the following code would also work

public MyViewModel(Func<ViewModelBase> viewModelCreator)
{
    var action = () => { creator = viewModelCreator; SetMainContent(creator()); };
    MyCommand = new RelayCommand(action);
}

If so, then the reason it isn't working the first way is that you aren't actually closing around the viewModelCreator variable, you're closing around the result of calling it.

I'm still playing around with the code in LINQPad, but it doesn't actually seem like I'm getting the same issue that you are. Perhaps it's something specific to RelayCommand. Could you post more of your code?

like image 88
Pat Avatar answered Oct 06 '22 00:10

Pat