«Длинная» задержка приводит к тому, что кнопка моего просмотра не активируется - PullRequest
2 голосов
/ 30 марта 2020

У меня есть список и кнопка в каждом столбце. Когда пользователь нажимает кнопку, он запускает асинхронное действие в модели представления, где я отключаю все кнопки и выполняю большое действие. Как только действие будет завершено, я снова включу их.

Однако если действие занимает слишком много времени, кнопки автоматически не включаются повторно, хотя я устанавливаю для свойства bound значение true и уведомляю о представлении. Если пользователь выполнит ЛЮБОЕ действие GUI после его завершения, кнопки снова включатся.

Еще одна странная вещь: если я делаю await Task.Delay вместо Thread.Sleep (Примечание: я делаю настоящую работу в полном приложении), он работает правильно.

Что что происходит здесь?

Я упростил здесь код, исключив модель (все логики c живет в ВМ).

Код модели представления:

namespace WpfTestApp
{
    public class viewmodel : INotifyPropertyChanged
    {

        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public RelayAsyncCommand<object> RunCommand { get; private set; }

        private ObservableCollection<subVM> _subVMs;
        public ObservableCollection<subVM> SubVMs
        {
            get => _subVMs; set
            {
                _subVMs = value;
                NotifyPropertyChanged();
            }
        }
        public viewmodel()
        {
            RunCommand = new RelayAsyncCommand<object>(OnRun);
            SubVMs = new ObservableCollection<subVM>
            {
                new subVM("ItemA"),
                new subVM("ItemB"),
            };
        }

        private async void OnRun(object o)
        {
            subVM vm = o as subVM;
            if (vm != null)
            {
                ChangeRunMode(false);
                Thread.Sleep(500);
            }
            ChangeRunMode(true);
        }

        private void ChangeRunMode(bool on)
        {
            foreach (subVM vm in SubVMs)
            {
                vm.ButtonEnabled = on;
            }
        }

    }

    public class subVM : INotifyPropertyChanged
    {
        private string name = "";
        public string Name
        {
            get => name;
            set
            {
                if (value != name)
                {
                    name = value;

                }
            }
        }
        public subVM(string name)
        {
            Name = name;
        }

        private bool tsk = true;
        public bool ButtonEnabled
        {
            get => tsk;
            set
            {
                if (tsk != value)
                {
                    tsk = value;
                    NotifyPropertyChanged("ButtonEnabled");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

    }
}

Просмотр XAML:

<Window x:Class="WpfTestApp.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:local="clr-namespace:WpfTestApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="130" Width="350">
    <Window.DataContext>
        <local:viewmodel/>
    </Window.DataContext>


        <ListView  Margin="5"
                   BorderBrush="DarkSlateGray" BorderThickness="1"
                   ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                   ItemsSource="{Binding SubVMs}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" 
                                        Width="200" DisplayMemberBinding ="{Binding Name}"/>
                    <GridViewColumn>
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Button
                                           Content="Load"
                                            IsEnabled="{Binding ButtonEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
                                           Margin="0"
                                           VerticalAlignment="Center"
                                            Command="{Binding Path=DataContext.RunCommand, IsAsync=True, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" 
                                            CommandParameter="{Binding}"
                                           />
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
</Window>

Вещи, которые я пробовал: Я пытался await Task.Run на своей реальной проблеме, и это все еще происходит.

RelayCommand и Asyn c версия (Я думал, что это были стандартные шаблоны, но здесь вы go):

public class RelayAsyncCommand<T> : RelayCommand<T>
{
    private bool isExecuting = false;

    public event EventHandler Started;

    public event EventHandler Ended;

    public bool IsExecuting
    {
        get { return this.isExecuting; }
    }

    public RelayAsyncCommand(Action<T> execute, Predicate<T> canExecute)
        : base(execute, canExecute)
    {
    }

    public RelayAsyncCommand(Action<T> execute)
        : base(execute)
    {
    }

    public override Boolean CanExecute(Object parameter)
    {
        return ((base.CanExecute(parameter)) && (!this.isExecuting));
    }

    public override void Execute(object parameter)
    {
        try
        {
            this.isExecuting = true;
            if (this.Started != null)
            {
                this.Started(this, EventArgs.Empty);
            }

            Task task = Task.Factory.StartNew(() =>
            {
                this._execute((T)parameter);
            });
            task.ContinueWith(t =>
            {
                this.OnRunWorkerCompleted(EventArgs.Empty);
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }
        catch (Exception ex)
        {
            this.OnRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
        }
    }

    private void OnRunWorkerCompleted(EventArgs e)
    {
        this.isExecuting = false;
        if (this.Ended != null)
        {
            this.Ended(this, e);
        }
    }
}


public class RelayCommand<T> : ICommand
{
    #region Fields

    readonly protected Action<T> _execute;
    readonly protected Predicate<T> _canExecute;

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<T> execute)
    : this(execute, null)
    {
    }

    public RelayCommand(Action<T> execute, Predicate<T> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public virtual bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute((T)parameter);
    }

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

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

    #endregion // ICommand Members
}

Ответы [ 2 ]

3 голосов
/ 01 апреля 2020

внутри метода OnRun вы фактически блокируете поток пользовательского интерфейса, предотвращая любое обновление / обновление пользовательского интерфейса; Вы должны await для длительной операции (и это именно то, что вы заметили, делая await Task.Delay):

private async void OnRun(object o)
{
    subVM vm = o as subVM;
    if (vm != null)
    {
        ChangeRunMode(false);
        await Task.Run(() =>
        {
            //put here your long operation as per your example
            for (int i = 0; i < 500; i++)
            {
                for (int k = 0; k < 100000; k++) ;
            }
        });
    }
    ChangeRunMode(true);
}

действительно, если вы присмотритесь к окну, вы увидите, что в настоящее время все зависает при длительном выполнении операции, не только отключаемые кнопки.

0 голосов
/ 02 апреля 2020

Я выяснил свою проблему:

Кнопка деактивации не приходит из моего связывания, когда я работаю асинхронно. Это исходит от RelayAsyncCommand х CanExecute. CanExecute здесь возвращает значение false во время выполнения задачи, но мы не запускаем запрос, когда оно выполнено.

Легко исправить, добавив закрытый набор в свойство IsExecuting, которое вызывает invalidate / Функция запроса на изменение (как и стандартный шаблон notifypropertychanged). Для потомков вот полный исправленный RelayAsyncCommand:

public class RelayAsyncCommand<T> : RelayCommand<T>
{
    private bool _isExecuting = false;

    public event EventHandler Started;

    public event EventHandler Ended;

    public bool IsExecuting
    {
        get { return _isExecuting; }
        private set
        {
            if (value != _isExecuting)
            {
                _isExecuting = value;
                CommandManager.InvalidateRequerySuggested();
            }
        }
    }

    public RelayAsyncCommand(Action<T> execute, Predicate<T> canExecute)
        : base(execute, canExecute)
    {
    }

    public RelayAsyncCommand(Action<T> execute)
        : base(execute)
    {
    }

    public override bool CanExecute(object parameter)
    {
        return ((base.CanExecute(parameter)) && (!IsExecuting));
    }

    public override void Execute(object parameter)
    {
        try
        {
            IsExecuting = true;
            Started?.Invoke(this, EventArgs.Empty);

            Task task = Task.Factory.StartNew(() =>
            {
                _execute((T)parameter);
            });

            task.ContinueWith(t =>
            {
                OnRunWorkerCompleted(EventArgs.Empty);
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }
        catch (Exception ex)
        {
            OnRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
        }
    }

    private void OnRunWorkerCompleted(EventArgs e)
    {
        IsExecuting = false;
        Ended?.Invoke(this, e);
    }
}

Спасибо Питеру за то, что он заставил меня взглянуть на (как я думал, был) шаблонный код.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...