【问题标题】:Unreproductible System.InvalidCastException in a ICommand implementation (Roslyn bug?)ICommand 实现中的不可复制 System.InvalidCastException(Roslyn 错误?)
【发布时间】:2018-12-11 17:58:03
【问题描述】:

我有一个非常奇怪的问题,我的测试失败了:

System.InvalidCastException:无法转换类型的对象 'c__DisplayClass18_0' 键入 'System.ComponentModel.INotifyPropertyChanged'。

但是,当我运行“调试测试”时,测试是绿色的,无论我是从单元测试还是实时测试“调试”运行调试。因此,调查起来很棘手。此外,我在应用程序中使用那些 ICommand 实现从未遇到任何问题。

我不应该有任何Exception(我没有任何单元测试,我有实时测试,并且在这种情况下 VS 没有达到(破坏)异常,即使它正在发生) .我应该如何前进?

演员表问题发生在DelegateCommandListen类中的ListenForNotificationFrom((INotifyPropertyChanged) _executeDelegate.Target);。

编辑:Action<T> 是私有命名函数 (1) 还是局部函数 (2) 或 lambda (2),其属性 Target 定义明确,应转换为 INotifyPropertyChanged。其中 (1) 适用于实时测试和单元测试,(2) 仅适用于单元测试。

我的ICommand 实现:

public class DelegateCommandListen : ICommand
{
    private readonly List<WeakReference> _controlEvent;
    private Action<object> _executeDelegate;

    public DelegateCommandListen(Action<object> executeDelegate, Predicate<object> canExecuteDelegate)
    {
        _controlEvent = new List<WeakReference>();
        ExecuteDelegate = executeDelegate;
        CanExecuteDelegate = canExecuteDelegate;
    }

    public Predicate<object> CanExecuteDelegate { get; set; }

    public Action<object> ExecuteDelegate
    {
        get { return _executeDelegate; }
        set
        {
            _executeDelegate = value;
            ListenForNotificationFrom((INotifyPropertyChanged) _executeDelegate.Target);
        }
    }

    public void RaiseCanExecuteChanged()
    {
        if (_controlEvent != null && _controlEvent.Count > 0)
            _controlEvent.ForEach(ce => { ((EventHandler) ce.Target)?.Invoke(null, EventArgs.Empty); });
    }

    public DelegateCommandListen ListenOn<TObservedType, TPropertyType>
        (TObservedType viewModel, Expression<Func<TObservedType, TPropertyType>> propertyExpression)
        where TObservedType : INotifyPropertyChanged
    {
        var propertyName = GetPropertyName(propertyExpression);
        viewModel.PropertyChanged += (s, e) =>
        {
            if (e.PropertyName == propertyName) RaiseCanExecuteChanged();
        };
        return this;
    }

    public void ListenForNotificationFrom<TObservedType>(TObservedType viewModel)
        where TObservedType : INotifyPropertyChanged
    {
        viewModel.PropertyChanged += (s, e) => RaiseCanExecuteChanged();
    }

    private static string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> expression)
        where T : INotifyPropertyChanged
    {
        var lambda = expression as LambdaExpression;
        var memberInfo = GetMemberExpression(lambda).Member;
        return memberInfo.Name;
    }

    private static MemberExpression GetMemberExpression(LambdaExpression lambda)
    {
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression body)
        {
            var unaryExpression = body;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
            memberExpression = lambda.Body as MemberExpression;
        return memberExpression;
    }

    public bool CanExecute(object parameter) => CanExecuteDelegate == null || CanExecuteDelegate(parameter);

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            _controlEvent.Add(new WeakReference(value));
        }
        remove
        {
            CommandManager.RequerySuggested -= value;
            _controlEvent.Remove(_controlEvent.Find(r => (EventHandler) r.Target == value));
        }
    }

    public void Execute(object parameter) => ExecuteDelegate?.Invoke(parameter);
}

这是我测试视图模型的方法:

[TestMethod]
public void NoTarget()
{
    var sut = new DummyViewModel();
    Assert.IsFalse(sut.IsSelected);
    Assert.IsFalse(sut.ListenWithoutTargetCommand.CanExecute(null));
    sut.IsSelected = true;
    Assert.IsTrue(sut.ListenWithoutTargetCommand.CanExecute(null));
}

视图模型:

public class DummyViewModel : INotifyPropertyChanged
{
    private ICommand _listenWith1TargetCommand;
    private bool _isSelected;

    public string Result { get; set; }

    public bool IsSelected
    {
        get => _isSelected;
        set
        {
            if (value == _isSelected) return;
            _isSelected = value;
            OnPropertyChanged();
        }
    }

    public ICommand ListenWith1TargetCommand
    {
        get
        {
            return _listenWith1TargetCommand ?? (_listenWith1TargetCommand = new DelegateCommandListen(
                           s => { Result = "Executing listen command 1"; }, // lambda|local function|named function
                           s => IsSelected)
                       .ListenOn(this, o => o.IsSelected));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

完整来源:https://github.com/mprevot/ReproLiveTests

【问题讨论】:

  • 调查起来并不难。看看错误信息告诉你什么。 &lt;&gt;c__DisplayClass18_0 似乎是一个匿名类型,它说它无法转换为 INotifyPropertyChanged
  • @RobertHarvey 1) VS/Roslyn 没有这么说(你的“它出现”是“文化”而不是逻辑)2)我不知道在没有中断的情况下你如何准确调查。
  • @RobertHarvey 和 Ash 你看到源了吗?我举了一个完整的例子。
  • 不在我的电脑上,所以不能检查这个,但是......在你初始化_listenWith1TargetCommand 的地方,尝试用对类方法的引用替换匿名委托。这些匿名委托可能正在编译为您在异常中看到的匿名类型(您可以使用 ILSpy/Reflector 之类的程序集检查工具进行检查)。所以匿名类型不能转换为INotifyPropertyChanged也就不足为奇了。而对 ViewModel 上的方法的引用可能会给出预期的目标。
  • @EmrahSüngü 好吧,那你应该去试试。有用。 OP 说当他进行单元测试时它不起作用。

标签: c# wpf visual-studio unit-testing


【解决方案1】:

根据 Artur Spychaj (MSFT) 的说法,测试失败是因为 LUT(实时单元测试)修改了 lambda 以捕获覆盖信息,并且编译器将 lambda 移动到一个单独的类中。解决方法是将this 作为DelegateCommandListen 的单独参数传递。

【讨论】:

    【解决方案2】:

    我们凡人通常看不到 Roslyn 错误 :) 因此,通过调查而不是指责工具来尝试解决手头的问题很重要 :) 我认为 Visual Studio 正在告诉您确切的问题。

    System.InvalidCastException:无法将“c__DisplayClass18_0”类型的对象转换为“System.ComponentModel.INotifyPropertyChanged”类型。

    而且,你几乎自己解决了这个问题。您提到匿名 lambda 不工作,但命名函数工作。

    让我们一一解决您的问题。首先什么是Action&lt;T&gt;.Target 属性。只需从 Visual Studio 复制和粘贴即可。

    当前委托调用实例方法的对象, 如果委托代表一个实例方法;如果委托为 null 表示静态方法。

    那么当你传递一个匿名 lambda 时,你认为会发生什么?我们如何首先传递匿名 lambda?好吧,你可能已经知道了,所以我就来刷新一下大家的记忆。后台编译器创建一个类,在您的情况下为 &lt;&gt;c__DisplayClass18_0 并将匿名 lambda 设置为所述生成类的属性并将其传递,就好像它们是适当类的命名函数一样!这很重要。

    那你的情况是怎么回事?当你传递一个匿名 lambda 时

    ListenForNotificationFrom((INotifyPropertyChanged) _executeDelegate.Target);
    

    _executeDelegate's.Target 是编译器生成的类'&lt;&gt;c__DisplayClass18_0',当然没有实现System.ComponentModel.INotifyPropertyChanged。这就是正在发生的事情。这不是 Roslyn 错误,实际上它应该是这样的:)

    编辑:只需添加一个调试屏幕截图。它是日文的,但我想你可以清楚地看到它与 OP 的信息相同。

    【讨论】:

    • 除非您尝试在调试器中重现它,否则它会按照 OP 的预期工作:) 并且您会获得 IsSelected 的容器类。我没有尝试进行单元测试,我只是尝试在调试器中重现它,因为我的理论与您相同。调试和发布版本都按照 OP 的建议工作。
    • 看来,当您运行单元测试时,行为与在调试器或独立运行时不同。这不会是第一次。
    • @SledgeHammer,您能否解释一下如何将以下Target 属性转换为INotifyPropertyChanged。 Action&lt;object&gt; act = (x) =&gt; { }; var casted = (INotifyPropertyChanged)act.Target;
    • 你所做的当然行不通。这与OP所做的完全不同。他传入了 s => IsSelected,其中 IsSelected 是实现 INPC 的类的成员。您在静态方法中传入了一个空操作。苹果和粉红色的火烈鸟与 OP 所做的一样。
    • @SledgeHammer,我决定不纠正:P 我已经克隆了 OP 的 repo 并检查了每个测试。我的回答可能无法反映测试用例中发生的情况,因为我们有一个 DUmmy 实现,但我 99% 确信实时代码与我在回答中解释的错误相同
    猜你喜欢
    • 2010-11-30
    • 2017-07-31
    • 2011-01-09
    • 2021-01-18
    • 1970-01-01
    • 2018-06-18
    • 2018-03-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多