@dhq_boiler

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【ReactiveProperty】ReactiveCommandでCanExecuteがtrueにならない

お世話になっております。

解決したいこと

C# + WPFでベクターグラフィックスドローイングツールを開発しています。

2021-07-09.png

※下にソースコードへの案内を記載しております。よろしければそちらを参照ください。

発生している問題・エラー

2021-07-09 (1).png

このツールに設定画面を実装しているところです。設定画面ではキャンパスの幅や高さ、ポイントにスナップするかどうかを設定できます。

その設定画面のSettingViewModel.csでReactivePropertyを使っております。OKボタンを押下できる条件は「キャンパスの幅と高さが乗算して0より大きくなったらOKボタンを押せるようにする」としたいのですが、ReactiveCommandの正しい書き方がわからず、立ち尽くしております。

ソースコード

boiler's Graphics
https://github.com/dhq-boiler/boiler-s-Graphics

gitリポジトリ
https://github.com/dhq-boiler/boiler-s-Graphics.git

ブランチ:develop

コミット:577fa7b

自分で試したこと

CombineLatest()でEditTargetのWidthとHeightを乗算して合体させてから、Select()で0より大きい時trueを返すように書きましたが、下記のGIF画像のように、OKボタンのCanExecuteがfalseになってしまっているようです。

SettingViewModel.cs
    class SettingViewModel : BindableBase, IDialogAware, IDisposable
    {
        private bool disposedValue;
        private CompositeDisposable _disposables = new CompositeDisposable();

        public ReactiveCommand OkCommand { get; set; }
        public ReactiveCommand CancelCommand { get; set; }

        public ReactiveProperty<Models.Setting> EditTarget { get; set; } = new ReactiveProperty<Setting>();

        public SettingViewModel()
        {
            EditTarget.Value = new Setting();
            CancelCommand = new ReactiveCommand();
            OkCommand = EditTarget.Value
                       .Width
                       .CombineLatest(EditTarget.Value.Height, (x, y) => x * y)
                       .Select(x => x > 0)
                       .ToReactiveCommand();
            OkCommand.Subscribe(_ =>
            {
                var parameters = new DialogParameters() { { "Setting", EditTarget.Value } };
                var ret = new DialogResult(ButtonResult.OK, parameters);
                RequestClose.Invoke(ret);
            })
            .AddTo(_disposables);
            CancelCommand.Subscribe(_ =>
            {
                var ret = new DialogResult(ButtonResult.Cancel, null);
                RequestClose.Invoke(ret);
            })
            .AddTo(_disposables);
        }

        public string Title => "設定";

        public event Action<IDialogResult> RequestClose;

        public bool CanCloseDialog()
        {
            return true;
        }

        public void OnDialogClosed()
        {
        }

        public void OnDialogOpened(IDialogParameters parameters)
        {
            EditTarget.Value = parameters.GetValue<Models.Setting>("Setting");
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    _disposables.Dispose();
                }

                _disposables = null;
                disposedValue = true;
            }
        }

        public void Dispose()
        {
            // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
Setting.xaml
<UserControl x:Class="boilersGraphics.Views.Setting"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:boilersGraphics.Views"
             xmlns:converter="clr-namespace:boilersGraphics.Converters"
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <converter:IntToStringConverter x:Key="IntToStringConverter" />
    </UserControl.Resources>
    <DockPanel>
        <StackPanel DockPanel.Dock="Bottom"
                    HorizontalAlignment="Right"
                    Orientation="Horizontal">
            <Button Command="{Binding OkCommand}"
                    Width="100"
                    Height="25"
                    Margin="5">OK</Button>
            <Button Command="{Binding CancelCommand}"
                    Width="100"
                    Height="25"
                    Margin="5">キャンセル</Button>
        </StackPanel>
        <StackPanel Orientation="Vertical">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="200" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Label Grid.Row="0" 
                       Grid.Column="0">キャンパスの幅</Label>
                <TextBox Grid.Row="0" 
                         Grid.Column="1"
                         HorizontalAlignment="Stretch"
                         HorizontalContentAlignment="Right"
                         Text="{Binding EditTarget.Value.Width.Value, Converter={StaticResource IntToStringConverter}}" />
                <Label Grid.Row="1"
                       Grid.Column="0">キャンパスの高さ</Label>
                <TextBox Grid.Row="1"
                         Grid.Column="1"
                         HorizontalAlignment="Stretch"
                         HorizontalContentAlignment="Right"
                         Text="{Binding EditTarget.Value.Height.Value, Converter={StaticResource IntToStringConverter}}" />
                <CheckBox Grid.Row="2"
                          Grid.Column="0"
                          Grid.ColumnSpan="2"
                          x:Name="enablePointSnap"
                          Margin="5"
                          IsChecked="{Binding EditTarget.Value.EnablePointSnap.Value}">ポイントにスナップする</CheckBox>
                <Label Grid.Row="3"
                       Grid.Column="0">ポイントにスナップする範囲</Label>
                <TextBox Grid.Row="3"
                         Grid.Column="1"
                         HorizontalAlignment="Stretch"
                         HorizontalContentAlignment="Right"
                         Text="{Binding EditTarget.Value.SnapPower.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                    <TextBox.Style>
                        <Style TargetType="{x:Type TextBox}">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding ElementName=enablePointSnap, Path=IsChecked}" Value="True">
                                    <Setter Property="IsReadOnly" Value="False" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding ElementName=enablePointSnap, Path=IsChecked}" Value="False">
                                    <Setter Property="IsReadOnly" Value="True" />
                                    <Setter Property="Foreground" Value="Gray" />
                                    <Setter Property="BorderBrush" Value="Gray" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBox.Style>
                </TextBox>
            </Grid>
            <StackPanel Orientation="Horizontal">
            </StackPanel>
            <StackPanel Orientation="Horizontal">
            </StackPanel>
        </StackPanel>
    </DockPanel>
</UserControl>

setting_width_height.gif

何か私の見落とし、致命的な勘違いなど気づいたところがあれば、回答していただけると助かります。よろしくお願いいたします。

0 likes

2Answer

自己解決しました。

OkCommandをReactivePropertyに変更し、EditTarget.Subscribe()でOkCommand.ValueにReactiveCommandをセット、OkCommand.Value.Subscribe()を実行し、さらにView側でキャンパスの幅テキストボックスとキャンパスの高さテキストボックスのバインディングにUpdateSourceTrigger=PropertyChangedを付けたところ、「キャンパスの幅と高さが乗算して0より大きくなったらOKボタンを押せるようにする」を実現することができました。

やや複雑なコードになりましたが、望み通りの動作が実現できたので満足しました。

SettingViewModel.cs
class SettingViewModel : BindableBase, IDialogAware, IDisposable
    {
        private bool disposedValue;
        private CompositeDisposable _disposables = new CompositeDisposable();

        public ReactiveProperty<ReactiveCommand> OkCommand { get; set; } = new ReactiveProperty<ReactiveCommand>();
        public ReactiveCommand CancelCommand { get; set; }

        public ReactiveProperty<Models.Setting> EditTarget { get; set; } = new ReactiveProperty<Setting>();

        public SettingViewModel()
        {
            EditTarget.Value = new Setting();
            CancelCommand = new ReactiveCommand();
            CancelCommand.Subscribe(_ =>
            {
                var ret = new DialogResult(ButtonResult.Cancel, null);
                RequestClose.Invoke(ret);
            })
            .AddTo(_disposables);
            EditTarget.Subscribe(_ =>
            {
                OkCommand.Value = EditTarget.Value
                             .Width
                             .CombineLatest(EditTarget.Value.Height, (x, y) => x * y)
                             .Select(x => x > 0)
                             .ToReactiveCommand();
                OkCommand.Value.Subscribe(__ =>
                {
                    var parameters = new DialogParameters() { { "Setting", EditTarget.Value } };
                    var ret = new DialogResult(ButtonResult.OK, parameters);
                    RequestClose.Invoke(ret);
                })
                .AddTo(_disposables);
            })
            .AddTo(_disposables);
        }

        public string Title => "設定";

        public event Action<IDialogResult> RequestClose;

        public bool CanCloseDialog()
        {
            return true;
        }

        public void OnDialogClosed()
        {
        }

        public void OnDialogOpened(IDialogParameters parameters)
        {
            EditTarget.Value = parameters.GetValue<Models.Setting>("Setting");
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    _disposables.Dispose();
                }

                _disposables = null;
                disposedValue = true;
            }
        }

        public void Dispose()
        {
            // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
Setting.xaml
<UserControl x:Class="boilersGraphics.Views.Setting"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:boilersGraphics.Views"
             xmlns:converter="clr-namespace:boilersGraphics.Converters"
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <converter:IntToStringConverter x:Key="IntToStringConverter" />
    </UserControl.Resources>
    <DockPanel>
        <StackPanel DockPanel.Dock="Bottom"
                    HorizontalAlignment="Right"
                    Orientation="Horizontal">
            <Button Command="{Binding OkCommand.Value}"
                    Width="100"
                    Height="25"
                    Margin="5">OK</Button>
            <Button Command="{Binding CancelCommand}"
                    Width="100"
                    Height="25"
                    Margin="5">キャンセル</Button>
        </StackPanel>
        <StackPanel Orientation="Vertical">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="200" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Label Grid.Row="0" 
                       Grid.Column="0">キャンパスの幅</Label>
                <TextBox Grid.Row="0" 
                         Grid.Column="1"
                         HorizontalAlignment="Stretch"
                         HorizontalContentAlignment="Right"
                         Text="{Binding EditTarget.Value.Width.Value, Converter={StaticResource IntToStringConverter}, UpdateSourceTrigger=PropertyChanged}" />
                <Label Grid.Row="1"
                       Grid.Column="0">キャンパスの高さ</Label>
                <TextBox Grid.Row="1"
                         Grid.Column="1"
                         HorizontalAlignment="Stretch"
                         HorizontalContentAlignment="Right"
                         Text="{Binding EditTarget.Value.Height.Value, Converter={StaticResource IntToStringConverter}, UpdateSourceTrigger=PropertyChanged}" />
                <CheckBox Grid.Row="2"
                          Grid.Column="0"
                          Grid.ColumnSpan="2"
                          x:Name="enablePointSnap"
                          Margin="5"
                          IsChecked="{Binding EditTarget.Value.EnablePointSnap.Value}">ポイントにスナップする</CheckBox>
                <Label Grid.Row="3"
                       Grid.Column="0">ポイントにスナップする範囲</Label>
                <TextBox Grid.Row="3"
                         Grid.Column="1"
                         HorizontalAlignment="Stretch"
                         HorizontalContentAlignment="Right"
                         Text="{Binding EditTarget.Value.SnapPower.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                    <TextBox.Style>
                        <Style TargetType="{x:Type TextBox}">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding ElementName=enablePointSnap, Path=IsChecked}" Value="True">
                                    <Setter Property="IsReadOnly" Value="False" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding ElementName=enablePointSnap, Path=IsChecked}" Value="False">
                                    <Setter Property="IsReadOnly" Value="True" />
                                    <Setter Property="Foreground" Value="Gray" />
                                    <Setter Property="BorderBrush" Value="Gray" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBox.Style>
                </TextBox>
            </Grid>
            <StackPanel Orientation="Horizontal">
            </StackPanel>
            <StackPanel Orientation="Horizontal">
            </StackPanel>
        </StackPanel>
    </DockPanel>
</UserControl>

setting_width_height_fixed.gif

0Like

これでいい気がしました。(Dispose は考慮してないコードです)

using Prism.Mvvm;
using Reactive.Bindings;
using System;
using System.Linq;
using System.Reactive.Linq;

namespace Qiita.Questions.No83581d540c85b24681ff
{
    public class MainWindowViewModel : BindableBase
    {
        public ReadOnlyReactivePropertySlim<string> Message { get; }
        public ReactivePropertySlim<Setting> EditTarget { get; } = new ReactivePropertySlim<Setting>();
        public ReactiveCommand OkCommand { get; }

        public ReactiveCommand ChangeEditTargetValueCommand { get; }
        public MainWindowViewModel()
        {
            EditTarget.Value = new Setting();

            OkCommand = EditTarget
                .Select(x => x.Width.CombineLatest(x.Height, (a, b) => a > 0 && b > 0))
                .Switch()
                .ToReactiveCommand();

            // Ok ボタンが押せたら更新されるプロパティを定義しているだけです(動作確認用)
            Message = OkCommand.Select(_ => DateTime.Now.ToString()).ToReadOnlyReactivePropertySlim();

            // EditTarget の値を更新するコマンド
            ChangeEditTargetValueCommand = new ReactiveCommand()
                .WithSubscribe(() => EditTarget.Value = new Setting());
        }
    }
}

reset.gif

コード全体:
https://github.com/runceel/Qiita.Questions.No83581d540c85b24681ff

0Like

Your answer might help someone💌