Reputation: 7197
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.
Upvotes: 3
Views: 508
Reputation: 1701
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.
Upvotes: 2
Reputation: 2230
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?
Upvotes: 0
Reputation: 12557
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?
Upvotes: 0