2

So I recently started working with MVVM, finally starting to get the concept down. I seem to have stumbled on to a minor issue with commands. I created a project that had a button and a textbox and the plan was to click the button and then generate some text to the textbox. Using MVVM ofcourse. So I managed to get it to work, the only issue is that with my button being bound to a command and what not, when clicking the button my UI froze, there are obvious reasons to why this is, but that got me thinking. To prevent UI deadlocks you would usually use async & await but with this, it started nagging about the method not being awaited in Execute(); in the Command that is. What is the proper way to deal with UI deadlocks like the one in this scenario? Would I make a AsyncCommand or.. ? Also if I need an AsyncCommand what's the propper way of creating one?

View

<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center">
    <Button Height="50"
            Width="150"
            Content="Add Product"
            VerticalAlignment="Center"
            HorizontalAlignment="Center"
            Command="{Binding AddProductCommand}"/>

    <TextBox Width="200"
             Margin="0,10,0,0"
             Text="{Binding MyProduct.ProductName}"/>

</StackPanel>

Model

public class ProductModel : INotifyPropertyChanged
    {
        private string _productName;
        private double _productPrice;


        public double ProductPrice
        {
            get { return _productPrice; }
            set
            {
                _productPrice = value;
                OnPropertyChanged("ProductPrice");
            }
        }

        public string ProductName
        {
            get { return _productName; }
            set
            {
                _productName = value;
                OnPropertyChanged("ProductName");
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;

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

ViewModel

public class ViewModel : INotifyPropertyChanged
    {
        private ProductService _productService;
        private ProductModel _myProduct;
        public AddProductCommand AddProductCommand { get; set; }



        public ViewModel()
        {
            _productService = new ProductService();
            _myProduct = new ProductModel();
            AddProductCommand = new AddProductCommand(this);

        }

        //Bind a button to a command that invokes this method.
        public void FillDescription()
        {
            _myProduct.ProductName = _productService.GetProductName();
        }

        public ProductModel MyProduct
        {
            get { return _myProduct; }
            set
            {
                _myProduct = value;
                OnPropertyChanged("MyProduct");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

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

Service

public class ProductService
{
    public ProductService()
    {

    }

    public string GetProductName()
    {
        var web = new HtmlWeb();
        var doc = web.Load("https://shop.supersimpleonline.com/products/baby-shark-official-plush");
        var title = doc.DocumentNode.SelectNodes("//h1[@itemprop = 'name']").FirstOrDefault(x => x != null).InnerText;
        return title;
    }
}

Command

public class AddProductCommand : ICommand
{
    public ViewModel ViewModel { get; set; }
    public AddProductCommand(ViewModel viewModel)
    {
        ViewModel = viewModel;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        ViewModel.FillDescription();
    }

    public event EventHandler CanExecuteChanged;
}
3
  • what happens if you call Task.Run() in your execute method? I haven't checked it, but think it should work Commented Aug 7, 2018 at 20:15
  • @mahlatse That's no good, that just fires and forgets the Task, I could be losing an exception or someone could press your button again.. Sudden race condition Commented Aug 7, 2018 at 20:36
  • for the exception part, you can use the continuewith and get the exception object Commented Aug 7, 2018 at 21:07

2 Answers 2

3

Here's a relay command that allows you to perform asynchronous work and keeps the command from firing multiple times / also shows the button disabled ETC. It's pretty straight forward.

public class AsyncRelayCommand : ICommand
{
    public Func<object, Task> ExecuteFunction { get; }
    public Predicate<object> CanExecutePredicate { get; }
    public event EventHandler CanExecuteChanged;
    public void UpdateCanExecute() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    public bool IsWorking { get; private set; }

    public AsyncRelayCommand(Func<object, Task> executeFunction) : this(executeFunction, (obj) => true) { }
    public AsyncRelayCommand(Func<object, Task> executeFunction, Predicate<object> canExecutePredicate)
    {
        ExecuteFunction = executeFunction;
        CanExecutePredicate = canExecutePredicate;
    }

    public bool CanExecute(object parameter) => !IsWorking && (CanExecutePredicate?.Invoke(parameter) ?? true);
    public async void Execute(object parameter)
    {
        IsWorking = true;
        UpdateCanExecute();

        await ExecuteFunction(parameter);

        IsWorking = false;
        UpdateCanExecute();
    }
}

The ViewModel can also use the IsWorking property on the AsyncRelayCommand to help the View with other working logic if needed.

public class ViewModel : INotifyPropertyChanged
{
    private bool asyncCommandWorking;
    public event PropertyChangedEventHandler PropertyChanged;
    public void Notify([CallerMemberName] string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    public ViewModel()
    {
        AsyncCommand = new AsyncRelayCommand(Execute, CanExecute);
    }

    private Task Execute(object obj)
    {
        return Task.Run(() =>
        {
            // do some work...
        });
    }

    private bool CanExecute(object obj)
    {
        AsyncCommandWorking = AsyncCommand.IsWorking;
        // process other can execute logic.
        // return the result of CanExecute or not
    }

    public AsyncRelayCommand AsyncCommand { get; }
    public bool AsyncCommandWorking
    {
        get => asyncCommandWorking;
        private set
        {
            asyncCommandWorking = value;
            Notify();
        }
    }
}

Hope this helps :)

Sign up to request clarification or add additional context in comments.

Comments

2

Create a generic command class that accepts an Action<object> to be invoked when then command is executed:

public class DelegateCommand : System.Windows.Input.ICommand
{
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;

    public DelegateCommand(Action<object> execute)
        : this(execute, null)
    {
        _execute = execute;
    }

    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        if (_canExecute == null)
            return true;

        return _canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    public event EventHandler CanExecuteChanged;
}

You could then pass in whatever Action<object> you want, including a method that calls your service method on a background thread, e.g.:

public class ViewModel : INotifyPropertyChanged
{
    public DelegateCommand AddProductCommand { get; set; }

    public ViewModel()
    {
        _productService = new ProductService();
        _myProduct = new ProductModel();
        AddProductCommand = new DelegateCommand(FillDescription);
    }

    async void FillDescription(object _)
    {
        try
        {
            await Task.Run(() => _myProduct.ProductName = _productService.GetProductName());
        }
        catch(Exception)
        {
            //...
        }
    }
}

3 Comments

Why the empty constructor? The first one that is
Also.. Isnt public async void bad practice? I thought you should always return a Task<type>
@MarkDenom: The constructor shouldn't be empty and FillDescription shouldn't be public. I fixed this. The only reason why the FillDescription method is marked as async is for you to be able to use the await keyword in it. Since the Execute method of ICommand is not async, it won't be awaited when command is executed by the framework.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.