質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.49%

  • C#

    7414questions

    C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

  • Xamarin

    518questions

    Xamarin(ザマリン)は、iPhoneなどのiOSやAndroidで動作し、C# 言語を用いてアプリを開発できるクロスプラットフォーム開発環境です。Xamarin Studioと C# 言語を用いて、 iOS と Android の両方の開発を行うことができます。

[Xamarin.Forms]ReactiveProperty<独自クラス>のBindingとObservableCollectionのVMとM双方向同期

解決済

回答 2

投稿

  • 評価
  • クリップ 0
  • VIEW 161

roamschemer

score 3

 前提・実現したいこと

Xamarin.FormsをPrism(MVVM)+ReactivePropertyで書いています。
Age、Name、Genderを入力または選択後、PushTappedでListViewに追加するような機能を作成しているのですが以下二つの問題点が発生しましたのでご助言を求めます。

1.ReactiveProperty<独自クラス>の場合のVとVMのBindingがうまくいきません。
2.ObservableCollectionのVMとMの双方向同期の記述方法がわかりません。

 試したこと

1.に関しては独自クラスを使わずintやstringで分離した場合は正しく動作しました。以下コードでは正しく動いておらず、VMのInputPersonが常に空白になってしまいますのでPushTappedでは空白のListViewが追加されます。

2.に関しては以下にあるようにToReadOnlyReactiveCollectionを使ってVM←Mでは再現できましたが、その場合Model側にもInputPersonプロパティが必要になっていて、これが無駄な気がしています。VMのListViewに追加するだけでM側も同期して欲しいのです。

 該当のソースコード

[View]

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="JsonSample.Views.MainPage"
             Title="JsonTest">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>
        <StackLayout Grid.Column="0">
            <Picker Title="年齢" ItemsSource="{Binding AgeList}" SelectedItem="{Binding InputPerson.Age.Value}"/>
            <Entry Placeholder="名前" Text ="{Binding InputPerson.Name.Value}" Keyboard="Text"/>
            <Picker Title="性別" ItemsSource="{Binding GenderList}" SelectedItem="{Binding InputPerson.Gender.Value}" />
            <Button Text="追加" Command="{Binding PushTapped}"/>
        </StackLayout>
        <ListView Grid.Column="1" ItemsSource="{Binding ListView}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="3*" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Label Grid.Column="0" Text="{Binding Age}" />
                            <Label Grid.Column="1" Text="{Binding Name}" />
                            <Label Grid.Column="2" Text="{Binding Gender}" />
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>

</ContentPage>


[ViewModel]

using JsonSample.Models;
using Prism.Navigation;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Collections.ObjectModel;

namespace JsonSample.ViewModels
{
    public class MainPageViewModel : ViewModelBase, IDisposable
    {
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();

        public ReadOnlyReactiveCollection<Person> ListView { get; private set; }
        public ObservableCollection<int> AgeList { get; private set; } = new ObservableCollection<int>();
        public ObservableCollection<string> GenderList { get; private set; } = new ObservableCollection<string>();

        public ReactiveProperty<Person> InputPerson { get; private set; } = new ReactiveProperty<Person>();

        public ReactiveCommand PushTapped { get; private set; } = new ReactiveCommand();

        private Commander commander = new Commander();

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            //Pickerリスト作成
            AgeList = new ObservableCollection<int>(Enumerable.Range(1, 100).ToList());
            GenderList = new ObservableCollection<string>() { "男", "女", "不明" };

            //ViewModel←Model
            ListView = commander.Commanders.ToReadOnlyReactiveCollection().AddTo(this.Disposable); //本当は双方向にしたかった

            //ViewModel→Model
            InputPerson = ReactiveProperty.FromObject(commander, x => x.InputPerson).AddTo(this.Disposable);

            //Button
            PushTapped.Subscribe(_ => commander.Push());
            //PushTapped.Subscribe(_ => ListView.Add(InputPerson)); 双方向だったならこんな感じで行けた?

        }

        public void Dispose()
        {
            this.Disposable.Dispose();
        }
    }
}


[Model]

using Prism.Mvvm;
using System.Collections.ObjectModel;

namespace JsonSample.Models
{
    public class Person : BindableBase
    {
        private int age;
        public int Age
        {
            get => age;
            set => SetProperty(ref age, value);
        }

        private string name;
        public string Name
        {
            get => name;
            set => SetProperty(ref name, value);
        }

        private string gender;
        public string Gender
        {
            get => gender;
            set => SetProperty(ref gender, value);
        }
    }

    public class Commander : BindableBase
    {
        private Person inputPerson;
        public Person InputPerson
        {
            get => inputPerson;
            set => SetProperty(ref inputPerson, value);
        }

        public ObservableCollection<Person> Commanders { get; private set; } = new ObservableCollection<Person>();

        public void Push()
        {
            Commanders.Add(InputPerson);
        }

    }

}

 その他

初心者である為、上記質問以外で直したほうが良いところがもしあればご指摘いただけると助かります。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+1

バインドしてもCommanderInputPersonのインスタンスが作られることはありません。
素直にボタン押したら、Personを作って追加するようにしています。

[View]

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="JsonSample.Views.MainPage"
             Title="JsonTest">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>
        <StackLayout Grid.Column="0">
            <Picker Title="年齢" ItemsSource="{Binding AgeList}" SelectedItem="{Binding Age.Value}"/>
            <Entry Placeholder="名前" Text ="{Binding Name.Value}" Keyboard="Text"/>
            <Picker Title="性別" ItemsSource="{Binding GenderList}" SelectedItem="{Binding Gender.Value}" />
            <Button Text="追加" Command="{Binding PushTapped}"/>
        </StackLayout>
        <ListView Grid.Column="1" ItemsSource="{Binding ListView}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="3*" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Label Grid.Column="0" Text="{Binding Age}" />
                            <Label Grid.Column="1" Text="{Binding Name}" />
                            <Label Grid.Column="2" Text="{Binding Gender}" />
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>

</ContentPage>

[ViewModel]

using JsonSample.Models;
using Prism.Navigation;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Collections.ObjectModel;

namespace JsonSample.ViewModels
{
    public class MainPageViewModel : ViewModelBase, IDisposable
    {
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();

        public ReadOnlyReactiveCollection<Person> ListView { get; private set; }
        public ObservableCollection<int> AgeList { get; private set; } = new ObservableCollection<int>();
        public ObservableCollection<string> GenderList { get; private set; } = new ObservableCollection<string>();

        //追加
        public ReactiveProperty<int> Age { get; } = new ReactiveProperty<int>();
        public ReactiveProperty<string> Gender { get; } = new ReactiveProperty<string>();
        public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();

        public ReactiveCommand PushTapped { get; private set; } = new ReactiveCommand();

        private Commander commander = new Commander();

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            //Pickerリスト作成
            AgeList = new ObservableCollection<int>(Enumerable.Range(1, 100).ToList());
            GenderList = new ObservableCollection<string>() { "男", "女", "不明" };

            //ViewModel←Model
            ListView = commander.Commanders.ToReadOnlyReactiveCollection().AddTo(this.Disposable); //本当は双方向にしたかった

            //Button
            // 修正
            PushTapped.Subscribe(_ => commander.Push(Age.Value, Gender.Value, Name.Value));
            //PushTapped.Subscribe(_ => ListView.Add(InputPerson)); 双方向だったならこんな感じで行けた?

        }

        public void Dispose()
        {
            this.Disposable.Dispose();
        }
    }
}

[Model]

using Prism.Mvvm;
using System.Collections.ObjectModel;

namespace JsonSample.Models
{
    public class Person : BindableBase
    {
        private int age;
        public int Age
        {
            get => age;
            set => SetProperty(ref age, value);
        }

        private string name;
        public string Name
        {
            get => name;
            set => SetProperty(ref name, value);
        }

        private string gender;
        public string Gender
        {
            get => gender;
            set => SetProperty(ref gender, value);
        }
    }

    public class Commander : BindableBase
    {
        public ObservableCollection<Person> Commanders { get; private set; } = new ObservableCollection<Person>();

        //修正
        public void Push(int age, string name, string gender)
        {
            var person = new Person
            {
                Age = age,
                Name = name,
                Gender = gender
            };

            Commanders.Add(person);
        }

    }

}

あと、気になったところで、MainPageViewModelIDisposableを実装していますけど、どこかで、Disposeの呼び出しを行なっていますか?そうならそれでもいいですが、IDestructibleを実装しておけば、ページが破棄されたときにDestroyが呼び出されるので、そこで処理を行うのでもいいです。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/11/09 08:47 編集

    やっぱりViewModelでAge,Name,GenderをそれぞれReactiveProperty型にする必要があるんですね。なんか勿体ないなとPersonクラスを使う方法を考えてみたんですが…
    でもこの方法ならModel側にInputPersonが不要なので手間的に言えばこれがベストですね。ありがとうございました。

    後全く別の話になってしまって恐縮ですが、IDisposableに関してはReactivePropertyでModelと同期する場合にとりあえず付けておけば安心程度に考えていました。以下を参考にしています。

    かずきのBlog@hatena ReactivePropertyの後始末
    https://blog.okazuki.jp/entry/2016/04/30/073755

    Prismのテンプレートを使う場合、ViewModelBaseクラスにIDestructibleが最初から実装されているので、こっちにあるDestroyメソッドに記述する方が良いのですかね?その場合どう書けば良いのでしょうか?

    キャンセル

  • 2018/11/09 10:19

    Destroyをオーバーライドして、そこに書けばいいです。

    キャンセル

  • 2018/11/09 15:38

    成程。ありがとうございました。

    キャンセル

+1

こんにちは!

<1について>
Viewのバインド設定が間違ってませんか?
InputPersonはReactiveProperty、
PersonクラスのAgeプロパティはReactivePropertyではないので

誤 {Binding InputPerson.Age.Value}
正 {Binding InputPerson.Value.Age}

名前、性別等も同じです。

<2について>
ReactiveCollection(ObsrvableCollection)のアイテムを双方向で同期する機能はなかった気がします。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/11/09 08:34

    {Binding InputPerson.Value.Age}としても直らなかったです。ObsrvableCollectionは無いんですね。どおりでいくら探しても見つからないはずです。ありがとうございました。

    キャンセル

  • 2018/11/09 09:08

    {Binding InputPerson.Value.Age}でだめなのはf-miyuさんが回答されましたが、InputPersonのValueにPersonのインスタンスが設定されていないためです。

    キャンセル

  • 2018/11/09 10:08

    申し訳ございませんが
    「InputPersonのValueにPersonのインスタンスが設定されていない」
    という意味が分かりません。この場合どこにどのように書けば良いのでしょうか?

    キャンセル

  • 2018/11/09 10:31 編集

    InputPersonはずっとnullのままということです。
    InputPersonを使うのであれば、Commanderのコンストラクタで初期化しておけばいいです。
    この場合でも、Push時では、別のPersonのインスタンスを追加するべきなので、Addする前にInputPersonの値を使って、Personを作るか、Addした後にInputPersonに新しいインスタンスを代入するかしないといけないです。
    もっとも、自分の回答のようにシンプルに引数を受け取った方がいいとは思いますが。

    キャンセル

  • 2018/11/09 11:04

    この場合以下で行けました。
    ```C#
    public Commander()
    {
    InputPerson = new Person();
    }

    public void Push()
    {
    var person = new Person
    {
    Age = InputPerson.Age,
    Name = InputPerson.Name,
    Gender = InputPerson.Gender
    };
    Commanders.Add(person);
    }
    ```
    成程、簡単に出来るかと思いましたが面倒になってしまいました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.49%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • C#

    7414questions

    C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

  • Xamarin

    518questions

    Xamarin(ザマリン)は、iPhoneなどのiOSやAndroidで動作し、C# 言語を用いてアプリを開発できるクロスプラットフォーム開発環境です。Xamarin Studioと C# 言語を用いて、 iOS と Android の両方の開発を行うことができます。