Используйте ICommand в WPF

Каков наилучший способ использовать команды в WPF?

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

есть мой MainWindow.xaml:

<Window ...>
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>          
        <Button Grid.Row="0"
                Grid.Column="0"
                Style="{StaticResource StyleButton}"
                Content="Load"
                Command="{Binding LoadCommand}"/>
        <Button Grid.Row="0"
                Grid.Column="1"
                Style="{StaticResource StyleButton}"
                Content="Generate"
                Command="{Binding GenerateCommand}"/>
    </Grid>
</Window>

и мой MainViewModel.cs:

public class MainViewModel : ViewModelBase
{

    #region GenerateCommand
    #endregion

    #region Load command
    private ICommand _loadCommand;
    public ICommand LoadCommand
    {
        get
        {
            if (_loadCommand == null)
                _loadCommand = new RelayCommand(OnLoad, CanLoad);
            return _loadCommand;
        }
    }

    private void OnLoad()
    {
        //My code
    }
    private bool CanLoad()
    {
        return true;
    }
    #endregion
}

Я видел решение с фоновым работником, но я не знаю, как его использовать. И мне интересно, если я должен создать один экземпляр по команде.

Есть ли чище / лучший способ?

4 ответа

Решение

Я хочу, чтобы мое приложение не зависало во время работы, но я хочу, чтобы функции были отключены.

Ключом к предотвращению зависания приложения является выполнение любой длительной операции в фоновом потоке. Самый простой способ сделать это - запустить задание. Чтобы отключить окно, вы можете привязать его свойство IsEnabled к исходному свойству модели представления, которую вы установили перед запуском задачи. Следующий пример кода должен дать вам идею:

public class MainViewModel : ViewModelBase
{
    private RelayCommand _loadCommand;
    public ICommand LoadCommand
    {
        get
        {
            if (_loadCommand == null)
                _loadCommand = new RelayCommand(OnLoad, CanLoad);
            return _loadCommand;
        }
    }

    private void OnLoad()
    {
        IsEnabled = false;
        _canLoad = false;
        _loadCommand.RaiseCanExecuteChanged();

        Task.Factory.StartNew(()=> { System.Threading.Thread.Sleep(5000); })  //simulate som long-running operation that runs on a background thread...
            .ContinueWith(task =>
            {
                //reset the properties back on the UI thread once the task has finished
                IsEnabled = true;
                _canLoad = true;
            }, System.Threading.CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
    }

    private bool _canLoad = true;
    private bool CanLoad()
    {
        return _canLoad;
    }

    private bool _isEnabled;
    public bool IsEnabled
    {
        get { return _isEnabled; }
        set { _isEnabled = value; RaisePropertyChanged(); }
    }
}

Обратите внимание, что вы не можете получить доступ к любому элементу пользовательского интерфейса из фонового потока, так как элементы управления имеют сходство потоков: http://volatileread.com/Thread/Index?id=1056

Мой подход, позволяющий избежать зависания пользовательского интерфейса в этих сценариях, заключается в использовании async / await при выполнении ICommand и выполнении долговременного кода в фоновом потоке. Ваш модифицированный код будет выглядеть примерно так:

public ICommand LoadCommand
{
    get
    {
        if (_loadCommand == null)
            _loadCommand = new RelayCommand(async o => await OnLoadAsync(), CanLoad);
        return _loadCommand;
    }
}

private async Task OnLoadAsync()
{
    await Task.Run(() => MyLongRunningProcess());
}

Если этой фоновой задаче необходимо обновить что-либо, связанное с пользовательским интерфейсом, то ее нужно обернуть в Dispatcher.Invoke (или же Dispatcher.BeginInvoke).

Если вы хотите предотвратить выполнение команды во второй раз, просто установите для "CanLoad" значение true, прежде чем await Task.Run(... линия и обратно к ложному после нее.

Я бы предложил использовать Akka.Net: вы можете найти пример с WPF на github.

Я раздвоил это, чтобы помешать командам остановки и запуска: моей целью было показать двунаправленную связь между актерами Akka.Net и ViewModel.

Вы увидите, что ViewModel вызывает ActorSystem вот так

    private void StartCpuMethod() {
        Debug.WriteLine("StartCpuMethod");
        ActorSystemReference.Start();
    }
    private void StopCpuMethod() {
        Debug.WriteLine("StopCpuMethod");
        ActorSystemReference.Stop();
    }

с актером, получающим эти сообщения

    public CPUReadActor()
    {
        Receive<ReadCPURequestMessage>(msg => ReceiveReadDataMessage());
        Receive<ReadCPUSyncMessage>(msg => ReceiveSyncMessage(msg));
    }

    private void ReceiveSyncMessage(ReadCPUSyncMessage msg)
    {
        switch (msg.Op)
        {
            case SyncOp.Start:
                OnCommandStart();
                break;
            case SyncOp.Stop:
                OnCommandStop();
                break;
            default:
                throw new Exception("unknown Op " + msg.Op.ToString());
        }
    }

и наоборот от актера

    public ChartingActor(Action<float, DateTime> dataPointSetter)
    {
        this._dataPointSetter = dataPointSetter;

        Receive<DrawPointMessage>(msg => ReceiveDrawPointMessage(msg));
    }

    private void ReceiveDrawPointMessage(DrawPointMessage msg)
    {
        _dataPointSetter(msg.Value, msg.Date);
    }

в ViewModel

    public MainWindowViewModel()
    {
        StartCpuCommand = new RelayCommand(StartCpuMethod);
        StopCpuCommand = new RelayCommand(StopCpuMethod);

        SetupChartModel();
        Action<float, DateTime> dataPointSetter = new Action<float, DateTime>((v, d) => SetDataPoint(v, d));

        ActorSystemReference.CreateActorSystem(dataPointSetter);
    }

    private void SetDataPoint(float value, DateTime date)
    {
        CurrentValue = value;
        UpdateLineSeries(value, date);
    }

На мой взгляд, лучше всего использовать async/await. https://msdn.microsoft.com/ru-ru/library/mt674882.aspx

public class MainViewModel : ViewModelBase
{

    public MainViewModel()
    {
        LoadCommand = new RelayCommand(async ol => await OnLoadAsync(), CanLoad);
    }

    public ICommand LoadCommand { get; }

    private async void OnLoadAync()
    {
        await SomethingAwaitable();
    }

    private Task<bool> SomethingAwaitable()
    {
        //Your code
    }

}
Другие вопросы по тегам