【问题标题】:Async ICommand delegate for MVVM Pattern with return value带有返回值的 MVVM 模式的异步 ICommand 委托
【发布时间】:2016-12-07 10:11:12
【问题描述】:

我刚开始用 C# 编程,想从一开始就做任何事情。 所以我了解了 MVVM 模式并试图让我的程序使用它。 为此,我使用了来自 ICommand 的 Delegate 和 lambda 函数。

这很好用,但由于我的程序使用 HTML 请求,我必须找到一种方法来异步调用命令。

所以我找到了一个很好的实现,我试图理解它,但我有一次失败了……下面是实现:

    public interface IRaiseCanExecuteChanged
    {
        void RaiseCanExecuteChanged();
    }

    // And an extension method to make it easy to raise changed events
    public static class CommandExtensions
    {
        public static void RaiseCanExecuteChanged(this ICommand command)
        {
            var canExecuteChanged = command as IRaiseCanExecuteChanged;

            if (canExecuteChanged != null)
                canExecuteChanged.RaiseCanExecuteChanged();
        }
    }

    public class DelegateCommand : DelegateCommand<object>
    {
        public DelegateCommand(Action executeMethod)
            : base(o => executeMethod())
        {
        }

        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
            : base(o => executeMethod(), o => canExecuteMethod())
        {
        }
    }

    /// <summary>
    /// A command that calls the specified delegate when the command is executed.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class DelegateCommand<T> : ICommand, IRaiseCanExecuteChanged
    {
        private readonly Func<T, bool> _canExecuteMethod;
        private readonly Action<T> _executeMethod;
        private bool _isExecuting;

        public DelegateCommand(Action<T> executeMethod)
            : this(executeMethod, null)
        {
        }

        public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
        {
            if ((executeMethod == null) && (canExecuteMethod == null))
            {
                throw new ArgumentNullException("executeMethod", @"Execute Method cannot be null");
            }
            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
        }

        public event EventHandler CanExecuteChanged
        {
            add
            {
                CommandManager.RequerySuggested += value;
            }
            remove
            {
                CommandManager.RequerySuggested -= value;
            }
        }

        public void RaiseCanExecuteChanged()
        {
            CommandManager.InvalidateRequerySuggested();
        }

        bool ICommand.CanExecute(object parameter)
        {
            return !_isExecuting && CanExecute((T)parameter);
        }

        void ICommand.Execute(object parameter)
        {
            _isExecuting = true;
            try
            {
                RaiseCanExecuteChanged();
                Execute((T)parameter);
            }
            finally
            {
                _isExecuting = false;
                RaiseCanExecuteChanged();
            }
        }

        public bool CanExecute(T parameter)
        {
            if (_canExecuteMethod == null)
                return true;

            return _canExecuteMethod(parameter);
        }

        public void Execute(T parameter)
        {
            _executeMethod(parameter);
        }
    }

    public interface IAsyncCommand : IAsyncCommand<object>
    {
    }

    public interface IAsyncCommand<in T> : IRaiseCanExecuteChanged
    {
        Task ExecuteAsync(T obj);
        bool CanExecute(object obj);
        ICommand Command { get; }
    }

    public class AwaitableDelegateCommand : AwaitableDelegateCommand<object>, IAsyncCommand
    {
        public AwaitableDelegateCommand(Func<Task> executeMethod)
            : base(o => executeMethod())
        {
        }

        public AwaitableDelegateCommand(Func<Task> executeMethod, Func<bool> canExecuteMethod)
            : base(o => executeMethod(), o => canExecuteMethod())
        {
        }
    }

    public class AwaitableDelegateCommand<T> : IAsyncCommand<T>, ICommand
    {
        private readonly Func<T, Task> _executeMethod;
        private readonly DelegateCommand<T> _underlyingCommand;
        private bool _isExecuting;

        public AwaitableDelegateCommand(Func<T, Task> executeMethod)
            : this(executeMethod, _ => true)
        {
        }

        public AwaitableDelegateCommand(Func<T, Task> executeMethod, Func<T, bool> canExecuteMethod)
        {
            _executeMethod = executeMethod;
            _underlyingCommand = new DelegateCommand<T>(x => { }, canExecuteMethod);
        }

        public async Task ExecuteAsync(T obj)
        {
            try
            {
                _isExecuting = true;
                RaiseCanExecuteChanged();
                await _executeMethod(obj);
            }
            finally
            {
                _isExecuting = false;
                RaiseCanExecuteChanged();
            }
        }

        public ICommand Command { get { return this; } }

        public bool CanExecute(object parameter)
        {
            return !_isExecuting && _underlyingCommand.CanExecute((T)parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { _underlyingCommand.CanExecuteChanged += value; }
            remove { _underlyingCommand.CanExecuteChanged -= value; }
        }

        public async void Execute(object parameter)
        {
            await ExecuteAsync((T)parameter);
        }

        public void RaiseCanExecuteChanged()
        {
            _underlyingCommand.RaiseCanExecuteChanged();
        }
    }

现在我有两个问题。 1. 像他们那样实施它好不好?正如我所看到的,对象 T 只是用作函数参数,我可以将其传递给我的 lambda 函数。

我可以用这段代码做什么:

    private ObservableList<string, string> dict;
    private IAsyncCommand searchCommand;
    public async Task myFunction() {
        //changes global variable dict bound to view
    public IAsyncCommand MyCommand
    {
        get
        {
            if (myCommand == null)
            {
                myCommand = new AwaitableDelegateCommand(
                    () =>
                    {
                        return myFunction(myParameter);
                    });
            }
        return searchCommand;
        }
    }

其中 myFunction 编辑一个全局变量 dict,该变量通过带有 getter 和 setter 的函数 MyFunction 绑定到视图。我希望这是常见的做法,因为我只是发现它是这样的。 我们在这里返回任务,AwaitableDelegateCommand 在内部等待它,所以我们不必关心。但是我们无法访问任务及其结果,对吧?

如果 myFunction 不返回 void 而是一个变量,我怎么能做到这一点。 然后我必须得到任务的结果(在等待异步之后)并将其分配给列表。

代码如下所示:

    public async Task<ObservableDictionary<string, string>> myFunction() {
        var dict = new ObservableDictionary<string, string>();
        //do work....
        return dict;
    }
    public IAsyncCommand MyCommand
    {
        get
        {
            if (myCommand == null)
            {
                //what to do here to assign dict the result of the Task?
            }
        return searchCommand;
        }
    }

这将使我的代码更可重用,我希望它是好的 :) 希望有人可以帮助我。

编辑: 由于我的问题似乎不是很清楚,我尝试再次解释它。

我的方法返回一个值,我希望我的方法与 AwaitableDelegateCommand 异步运行

现在的问题...返回值的方法通常对其余代码不执行任何操作,但返回的值... 在我的例子中,它从 HTML 请求中填充字典并返回它。 如果我现在在 lambda 中运行异步方法,它什么也不做......因为我无法在后台获得任务的结果。 所以我现在通过重写函数解决了这个问题。它不是填充一个局部字典变量,而是填充一个全局变量。 这是我能弄清楚访问函数数据的唯一方法。 我只是想知道是否有其他方法可以做到这一点,或者这是否是唯一/最好的方法。

希望这会更好。

【问题讨论】:

    标签: c# wpf mvvm lambda


    【解决方案1】:

    wpf 中的ICommand 绑定是即发即弃的,也就是说,它们没有直接的返回值。命令的效果通过使用INotifyPropertyChanged从视图模型返回:

    internal class MyViewModel : INotifyPropertyChanged
    {
        public MyViewModel( IServer server )
        {
            MyCommand = new DelegateCommand( async () => MyProperty = await server.GetNewData() );
        }
    
        #region Bindings
        public string MyProperty
        {
            get
            {
                return _myProperty;
            }
            set
            {
                _myProperty = value;
                OnPropertyChanged();
            }
        }
    
        public ICommand MyCommand { get; }
        #endregion
    
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    
        #region private
        private string _myProperty;
    
        private void OnPropertyChanged( [CallerMemberName] string propertyName = null )
        {
            PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
        }
        #endregion
    }
    
    public interface IServer
    {
        Task<string> GetNewData();
    }
    

    MyView 会有这样的东西:

    <Button Content="Click me!" Command={Binding MyCommand}" />
    <TextBlock Text={Binding MyProperty} />
    

    现在,如果您单击按钮,将执行MyCommand,从服务器请求数据并在数据到达后更新MyPropertyMyProperty 触发对MyViewTextBlock 的更新,用户会看到新数据...

    编辑:

    在我的例子中,[方法]从 HTML 请求中填充字典并返回它。

    因此,该方法不适合(直接)用作命令中的委托。由命令调用的委托必须修改应用程序的状态,否则该命令什么也不做,正如您所说的那样。因此,您应该创建一个带有委托的命令,该委托调用该方法并对返回值执行一些操作:

    MyCommand = new DelegateCommand( async () => _model.Add( await ParseDataFromRequest() );
    

    话虽如此,ParseDataFromRequest 无论如何都不应该是视图模型的成员,它属于应用程序的逻辑部分。视图模型应该只转发和调整数据以供视图使用。

    【讨论】:

    • 对不起,按回车键,无法及时编辑 -.-" 直到现在我才使用 OnPropertyChanged 的​​东西。如果我的字典得到新数据,那真的很有用;)但我的问题是我能不能获取具有返回值的函数与命令一起使用(即使其表现得像一个任务?)我的意思是实现完全过度工作:P 也许我可以更改一些代码来使其工作?或者它完全不可能?我只是不希望我的函数编辑全局变量以使其可重用于其他程序。
    • 为什么不能在命令中使用带有返回值的函数?只需丢弃返回值...new Command( () =&gt; ThisMethodReturnsSomething() )
    • 但我需要将返回值分配给我的全局变量。我只是想让它可重复使用。如果我使用该函数而不返回,它什么也不做,因为我返回的也是我创建/编辑的东西。我不想仅仅因为将它用作命令而改变我的方法。
    • 尽管使用全局变量是邪恶的,你可以写new Command( () =&gt; _globalVariable = ThisMethodWantsToModifyTheModel() )
    • 我的方法是异步的并返回一个任务。所以我可以得到这个任务。但我想在我的异步委托中在后台执行此操作。除此之外,我的问题是如何获得返回值......如果可以回答我不必使用全局变量:P
    【解决方案2】:

    由于您正在异步检索数据,我建议在我的Mvvm.Async library 中使用类似NotifyTask&lt;T&gt; 的内容。它基于我写的一篇关于 async data binding 的旧文章。

    例如,给定:

    public async Task<Result> MyFunctionAsync();
    

    你可以让你的 ViewModel 看起来像这样:

    public NotifyTask<Result> MyFunctionResults { get { ... } private set { ... /* raise PropertyChanged */ } }
    public ICommand StartMyFunction { get; } = new DelegateCommand(() =>
    {
      MyFunctionResults = NotifyTask.Create(MyFunctionAsync());
    });
    

    然后您的数据绑定将使用MyFunctionResults.Result 来显示结果。数据绑定还可以使用MyFunctionResults.IsNotCompleted 和其他实用程序属性来显示/隐藏微调器和错误指示器。

    请注意,这里不需要异步命令。该命令仅启动异步操作,异步数据绑定用于在数据到达时更新UI。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-02-17
      • 1970-01-01
      • 2013-10-02
      • 2014-11-01
      • 2011-09-25
      • 1970-01-01
      • 2017-06-14
      相关资源
      最近更新 更多