3

I am making a small MVVM application. It is supposed to take a class full of Func<string>, and display a list of buttons each executing a command containing a Func<string>, and displaying their return values in another list.

The program works fine at first but after a random amount of button presses it simply stops executing commands. The UI is still responsive. It is as if the binding broke.

There are a bit too many classes so I attached the whole project in the following link:

http://www.megafileupload.com/en/file/403770/GenericTester-zip.html

Relevant code:

namespace AdapterTester.ViewModel
{
  public class MainViewModel : ViewModelBase
  {
    public ObservableCollection<ViewableRelayCommand> CommandsList { get; set; }
    public ObservableCollection<string> Log { get; set; }

    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel()
    {
      CommandsList = new ObservableCollection<ViewableRelayCommand>();
      Log = new ObservableCollection<string>();
      MapCommands();
    }

    /// <summary>
    /// adds a ViewableRelayCommand to the CommandsList
    /// </summary>
    public void Add(Func<string> iCommand, string CommandName, Func<bool> CanExecute = null)
    {
      CommandsList.Add(new ViewableRelayCommand()
      {
        Command = new RelayCommand(() => { Log.Insert(0, "-------------\n" + CommandName  + "\n"  + (iCommand.Invoke())); }),
        CommandName = CommandName
      });
    }

    /// <summary>
    /// For Each Func<string> in TestFunctions create a ViewableRelayCommand
    /// </summary>
    private void MapCommands()
    {
      var target = new TestFunctions();
      var methods = target.GetType().GetMethods().Where(m => m.DeclaringType == typeof(TestFunctions));
      foreach (var method in methods)
      {
        if( (method.ReturnType == typeof(string)) && (method.GetParameters().Length ==0))
        {
          Func<string> func = (Func<string>)Delegate.CreateDelegate(typeof(Func<string>), target, method);
          Add(func, method.Name);
        }
      }
    }
  }

  public class ViewableRelayCommand : ViewModelBase
  {
    public RelayCommand Command { get; set; }

    /// <summary>
    /// The <see cref="CommandName" /> property's name.
    /// </summary>
    public const string CommandNamePropertyName = "CommandName";

    private string _CommandName = "Hello";

    /// <summary>
    /// Sets and gets the CommandName property.
    /// Changes to that property's value raise the PropertyChanged event. 
    /// </summary>
    public string CommandName
    {
      get
      {
        return _CommandName;
      }

      set
      {
        if (_CommandName == value)
        {
          return;
        }

        RaisePropertyChanging(CommandNamePropertyName);
        _CommandName = value;
        RaisePropertyChanged(CommandNamePropertyName);
      }
    }
  }
}

XAML:

<Window x:Class="AdapterTester.MainWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:ignore="http://www.ignore.com"
      mc:Ignorable="d ignore"
      Width="500"
      Height="300"
      Title="MVVM Light Application"
      DataContext="{Binding Main, Source={StaticResource Locator}}">
  
  <Window.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Skins/MainSkin.xaml" />
      </ResourceDictionary.MergedDictionaries>
      <DataTemplate x:Key="myButtonTemplate">
        <Button Content="{Binding Path=CommandName}" Command="{Binding Path=Command}" Margin="3"></Button>
      </DataTemplate>
    </ResourceDictionary>
  </Window.Resources>

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <ListBox Name="CommandsListBox" Grid.Column="0" 
             ItemsSource="{Binding CommandsList}"
             ItemTemplate="{StaticResource myButtonTemplate}">
    </ListBox>
     <ListBox Name="LogListBox" Grid.Column="1" 
                ItemsSource="{Binding Log}"
    </ListBox>
  </Grid>
</Window>

update:

answer is to change:

 Command = new RelayCommand(() => { Log.Insert(0, "-------------\n" + CommandName  + "\n"  + (iCommand.Invoke())); }),

to something like this:

List<Action> actions = new List<Action>();
public void Add(Func<string> iCommand, string CommandName, Func<bool> CanExecute = null)
{
    Action act = () => { Log.Insert(0, "-------------\n" + CommandName + "\n" + (iCommand.Invoke())); };
    actions.Add(act);
    CommandsList.Add(new ViewableRelayCommand()
    {
        Command = new RelayCommand(act)
        ,
        CommandName = CommandName
    });
}

Because the actions added to relay command where not rooted.

update Changing to my own relay command helped. Though rooting the Funcs did not.

peterh
  • 11,875
  • 18
  • 85
  • 108
Nahum
  • 6,959
  • 12
  • 48
  • 69
  • 1
    Modify your binding trace level as in this example: http://stackoverflow.com/a/343206/318795 and tell us, what is the debug output regarding that binding. – pbalaga Mar 20 '13 at 08:12
  • http://pastebin.com/n7J2Tp0a nothing changes after first load – Nahum Mar 20 '13 at 08:25
  • Sorry to say no at the moment. I have trouble compiling your code, *maybe* because you use Blend, which I have not? Did you set `TraceLevel=High` on `Command` or `Content` binding? Output suggests verbose info is printed for `CommandName`. But it is `Command` that we want to debug. – pbalaga Mar 20 '13 at 09:03
  • @rook I don't have blend. I just used the mvvm light template. I DO see that a few moments after buttons stop working a thread stops no idea withc thread – Nahum Mar 20 '13 at 09:11

3 Answers3

2

Is it using RelayCommand from MVVMLight?

If so, you could be hitting a GC issue. RelayCommand internally uses a WeakReference to its callbacks.

If you're passing in an anon function that's not rooted elsewhere, then it could be getting cleaned up when the GC runs.

Most of the time this isn't an issue because the func is a callback to the VM and the VM itself is rooted in the DataContext, the ViewModelLocator or elsewhere. If you're creating Func's that aren't rooted though, it could be an issue.

One way to root those Func's would be to have a List<Func<string>> in your ViewModel, and add them to the list at the same time you create the RelayCommands.

Claire Novotny
  • 1,593
  • 1
  • 15
  • 19
  • 1
    oren you where right about the cause but the suppppose to root the Actions created using the funcs and not the funcs :) – Nahum Mar 22 '13 at 15:34
  • Yes, of course, good catch. - it's because the WeakFunc/Action actually unwraps the original func and stores the values in WeakReferences. – Claire Novotny Mar 22 '13 at 16:34
0
Command = new RelayCommand(() => { Log.Insert(0, "-------------\n" + CommandName  + "\n"  + (Command.Invoke())); })

change to:

Command = new RelayCommand(() => { Log.Insert(0, "-------------\n"); })

it can works, but I don't know why can't use the parameter(Command and CommandName) in the Lambda expressions?

backslash112
  • 2,105
  • 1
  • 24
  • 29
0

Am I right that you invoke the command from within the command ?

 Command = new RelayCommand(() => { Log.Insert(0, "-------------\n" + CommandName  + "\n"  + (Command.Invoke())); })

Isn't that recursive?

Can you try to remove the invoke from the expression? and why are you invoking it from inside?

Boas Enkler
  • 12,264
  • 16
  • 69
  • 143
  • no, the command to the left is the ViewableRelayCommand Parameter. the command to the right is the Add function parameter. thats bad naming thats all. – Nahum Mar 22 '13 at 10:52