dhq_boiler
@dhq_boiler

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

[ReactiveProperty]ObserveElementObservableProperty()の使い方がわからない

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

解決したいこと

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

2021-07-23.png

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

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

現在、レイヤー機能を実装しているところなのですが、レイヤーに含まれるItemを全て集めたReactivePropertyの作り方がわからなくて困っています。

各クラスの数量の関係は次の通りです。

DiagramViewModel 1対多 Layer
Layer 1対多 LayerItem
LayerItem 1対1 SelectableDesignerItemViewModelBase

DiagramViewModelで選択中のレイヤーのItemを追加・削除すると、選択中のLayerの中のLayerItem(SelectableDesignerItemViewModelBaseを含む)を追加・削除します。その変更に伴って、総量が変動するDiagramViewModel.AllItemsプロパティを書きました。しかし、DiagramViewModelクラスのLayerコレクションのLayerItemコレクションのSelectableDesignerItemViewModelBaseインスタンスを変更検知しながらReactiveCollectionを生成するコードの書き方が、Web上のどこを探しても見つかりませんでした。

今のところ、こじらせた書き方をしてしまっているのが原因かはわからないですが、ItemをDeleteキーで削除した後、画面上にItemが残ってしまいます。デバッグしたところ、DiagramViewModel.AllItemsプロパティの総量が正しく同期されていないようです。しかし、Itemの追加時は正しく同期されているようです。

上記の方法について、心当たりのある方はいらっしゃいますでしょうか。よろしければ教えてください。

DiagramViewModel.cs

    public class DiagramViewModel : BindableBase, IDiagramViewModel, IDisposable
    {

        public DiagramViewModel()
        {
            :
            AllItems = Layers.ObserveElementObservableProperty(x => x.AllItemsObservable)
                             .Select(x => x.Value.Value)
                             .ToReadOnlyReactiveCollection();

            AllItems.ObserveAddChanged()
                    .Subscribe(x =>
                    {
                        Debug.WriteLine($"Added {x} into AllItems");
                    })
                    .AddTo(_CompositeDisposable);

            AllItems.ObserveRemoveChanged()
                    .Subscribe(x =>
                    {
                        Debug.WriteLine($"Removed {x} from AllItems");
                    })
                    .AddTo(_CompositeDisposable);
            :
        }
        :
        public ReadOnlyReactiveCollection<SelectableDesignerItemViewModelBase> AllItems { get; }
        :
    }
Layer.cs
    public class Layer : BindableBase
    {
        :
        public ReactiveCollection<LayerItem> Items { get; } = new ReactiveCollection<LayerItem>();

        public IObservable<PropertyPack<LayerItem, bool>> Observable
        {
            get { return Items.ObserveElementObservableProperty(x => x.Observable); }
        }
        public IObservable<PropertyPack<LayerItem, SelectableDesignerItemViewModelBase>> AllItemsObservable
        {
            get { return Items.ObserveElementObservableProperty(x => x.AllItemsObservable); }
        }

        public Layer()
        {
             :
        }
    }
LayerItem.cs
    public class LayerItem : BindableBase, IDisposable
    {
        :
        public ReactivePropertySlim<SelectableDesignerItemViewModelBase> Item { get; } = new ReactivePropertySlim<SelectableDesignerItemViewModelBase>();

        public IObservable<bool> Observable
        {
            get { return Item.ObserveProperty(x => x.Value.IsSelected); }
        }

        public IObservable<SelectableDesignerItemViewModelBase> AllItemsObservable
        {
            get { return System.Reactive.Linq.Observable.Return(Item.Value); }
        }
        :
    }
DiagramControl.xaml
<UserControl
    x:Class="boilersGraphics.UserControls.DiagramControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:attached="clr-namespace:boilersGraphics.AttachedProperties"
    xmlns:behavior="clr-namespace:boilersGraphics.Views.Behaviors"
    xmlns:control="clr-namespace:boilersGraphics.Controls"
    xmlns:converter="clr-namespace:boilersGraphics.Converters"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:boilersGraphics="clr-namespace:boilersGraphics"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:local="clr-namespace:boilersGraphics.UserControls"
    xmlns:helper="clr-namespace:boilersGraphics.Helpers"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:selector="clr-namespace:boilersGraphics.StyleSelectors"
    xmlns:view="clr-namespace:boilersGraphics.Views"
    xmlns:viewModel="clr-namespace:boilersGraphics.ViewModels"
    d:DesignHeight="450"
    d:DesignWidth="800"
    Loaded="DesignerCanvas_Loaded"
    mc:Ignorable="d">
    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
        <converter:PointsToAngleConverter x:Key="PointsToAngleConverter" />
    </UserControl.Resources>
    <Border BorderBrush="LightGray" BorderThickness="1">
        <Grid>
            <ScrollViewer Name="DesignerScrollViewer"
                          HorizontalScrollBarVisibility="Auto"
                          VerticalScrollBarVisibility="Auto">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="PreviewMouseWheel">
                        <helper:EventToCommand Command="{Binding MouseWheelCommand}" PassEventArgsToCommand="True" />
                    </i:EventTrigger>
                    <i:EventTrigger EventName="PreviewMouseDown">
                        <helper:EventToCommand Command="{Binding PreviewMouseDownCommand}" PassEventArgsToCommand="True" />
                    </i:EventTrigger>
                    <i:EventTrigger EventName="PreviewMouseUp">
                        <helper:EventToCommand Command="{Binding PreviewMouseUpCommand}" PassEventArgsToCommand="True" />
                    </i:EventTrigger>
                    <i:EventTrigger EventName="MouseMove">
                        <helper:EventToCommand Command="{Binding MouseMoveCommand}" PassEventArgsToCommand="True" />
                    </i:EventTrigger>
                    <i:EventTrigger EventName="MouseLeave">
                        <helper:EventToCommand Command="{Binding MouseLeaveCommand}" PassEventArgsToCommand="True" />
                    </i:EventTrigger>
                    <i:EventTrigger EventName="MouseEnter">
                        <helper:EventToCommand Command="{Binding MouseEnterCommand}" PassEventArgsToCommand="True" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
                <ScrollViewer.Background>
                    <VisualBrush Stretch="None"
                                 TileMode="Tile"
                                 Viewport="0,0,16,16"
                                 ViewportUnits="Absolute">
                        <VisualBrush.Visual>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="8" />
                                    <ColumnDefinition Width="8" />
                                </Grid.ColumnDefinitions>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="8" />
                                    <RowDefinition Height="8" />
                                </Grid.RowDefinitions>
                                <Rectangle Grid.Row="0"
                                           Grid.Column="0"
                                           Fill="#EEE" />
                                <Rectangle Grid.Row="0"
                                           Grid.Column="1"
                                           Fill="#AAA" />
                                <Rectangle Grid.Row="1"
                                           Grid.Column="0"
                                           Fill="#AAA" />
                                <Rectangle Grid.Row="1"
                                           Grid.Column="1"
                                           Fill="#EEE" />
                            </Grid>
                        </VisualBrush.Visual>
                    </VisualBrush>
                </ScrollViewer.Background>
                <ItemsControl ItemContainerStyleSelector="{x:Static selector:DesignerItemsControlItemStyleSelector.Instance}"
                              ItemsSource="{Binding AllItems, UpdateSourceTrigger=PropertyChanged}"
                              Background="Transparent"
                              BorderBrush="Black"
                              BorderThickness="{Binding CanvasBorderThickness}">
                    <ItemsControl.Resources>

                        :

                    </ItemsControl.Resources>

                    :

            </ScrollViewer>
            <StackPanel Orientation="Vertical"
                        HorizontalAlignment="Right"
                        VerticalAlignment="Top">
                <control:ZoomBox
                    x:Name="zoomBox"
                    Width="180"
                    Margin="0,5,25,0"
                    ScrollViewer="{Binding ElementName=DesignerScrollViewer}"
                    Visibility="{Binding EnableMiniMap.Value, Converter={StaticResource BoolToVisibilityConverter}}"/>
                <control:Combine
                    x:Name="combine"
                    Width="180"
                    Margin="0,5,25,0" />
                <control:Layers
                    x:Name="layers"
                    Width="180"
                    Margin="0,5,25,0" />
            </StackPanel>
        </Grid>
    </Border>
</UserControl>

以下のGifアニメを見ると、アイテムを選択した後、Deleteキーを押すと、レイヤーウィンドウからはアイテムが削除されたことがわかりますが、画面上にはアイテムが残っています。

item_removed_but_remain_in_view.gif

ソースコード

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

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

ブランチ:feature/Layer

コミット:78f32f7

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

0

3Answer

下記のコードでもビルドは通るのですが、実行時にArgumentException: 'propertySelector'と例外が出てしまいます。

DiagramViewModel.cs
AllItems = Layers.ObserveElementObservableProperty<Layer, ReactiveCollection<LayerItem>>(x => Observable.Return(x.Items))
                             .SelectMany(x => x.Value)
                             .Select(x => x.Item.Value)
                             .ToReadOnlyReactiveCollection();
0Like

すみません、回答じゃないですが、

        public IObservable<SelectableDesignerItemViewModelBase> AllItemsObservable
        {
            get { return System.Reactive.Linq.Observable.Return(Item.Value); }
        }

ここの作り方が怪しいように見えます。

LayerItemのインスタンスと、LayerItemが持っているSelectableDesignerItemViewModelBaseのインスタンスは1対1なのだから、ある1つのLayerが持っているすべてのLayerItemのSelectableDesignerItemViewModelBaseのコレクションある1つのLayerが持っているLayerItemのコレクションの射影(Selectで変換されたもの)として作るのが自然だと思います。ので、ここのコード見たときにかなり混乱しました(実を言うとまだよく分かってないので、この疑問自体が妥当なのかどうかよく分かってません)。

Itemプロパティに対応したIObservableを返したいだけなら単にReactivePropertySlim<SelectableDesignerItemViewModelBase> Itemの参照を返すだけではダメなんでしょうか?

0Like

Comments

  1. @dhq_boiler

    Questioner

    回答ありがとうございます。
    すみません、ReactiveProperty本家にIssueを投げてましてそちらで回答していただいてたのて、Qiitaで質問してたのを放置してしまっていました。Issueへのリンクを貼っておきますね。

Your answer might help someone💌