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

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

ただいまの
回答率

87.61%

JsonデータをMVVMで表示

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 3,247

score 202

やりたいこと

サーバよりJsonでデータを取得してMVVMで画面に表示したい。

Jsonデータは以下の通りです。

  • Code(データ取得状態コード) ※アプリ内共通
  • Message(エラーメッセージ) ※アプリ内共通
  • Data(詳細データ)※アプリ内各所個別

そのため、以下のようにクラスを作成

public class JsonData<T>
    {
        public int Code
        {
            get; set;
        }
        public string Message
        {
            get; set;
        }
        public T Data
        {
            get; set;
        }
    }
    public class OrgData: BindableBase
    {
        int _ID;
        public int ID
        {
            get
            {
                return _ID;
            }
            set
            {
                SetProperty(ref _ID, value);
            }
        }
        string _Text;
        public string Text
        {
            get
            {
                return _Text;
            }
            set
            {
                SetProperty(ref _Text, value);
            }
        }
    }
    public abstract class BindableBase :INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
        {
            if(object.Equals(storage, value))
                return false;

            storage = value;
            this.OnPropertyChanged(propertyName);
            return true;
        }
        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

作った環境

画面側は以下の通りです。
今回はテストのためEntryに入力したJson文字列をMVVMで画面するものを作成しています。

<?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="App1.Page1">
  <StackLayout x:Name="stackMain">
    <Entry x:Name="txtData" />
    <Label Text="{Binding ID}" />
    <Label Text="{Binding Text}" />
    <Button x:Name="btn1" 
            Text="GetData" />
  </StackLayout>
</ContentPage>
using System;
using Xamarin.Forms;
using Newtonsoft.Json;

namespace App1
{
    public partial class Page1 :ContentPage
    {
        public Page1()
        {
            InitializeComponent();
            var _vm = new JsonData<OrgData> { Data = new OrgData() };
            stackMain.BindingContext = _vm.Data;
            this.btn1.Clicked += (sender, e) =>
            {
                try
                {
                    var ret = txtData.Text;
                    _vm = JsonConvert.DeserializeObject<JsonData<OrgData>>(ret);
                }
                catch(Exception ex)
                {
                    DisplayAlert("error", ex.Message, "OK");
                }
            };
        }
    }
}

困っていること

_vm = JsonConvert.DeserializeObject<JsonData<OrgData>>(ret); の場所で、新しくオブジェクトが生成されるためか、PropertyChanged がnullとなり画面に反映されませんでした。
ちなみに、JsonConvertをせず、一つ一つプロパティをセットすると問題なく反映されました。

考えてみたこと

  1. Type.GetPropertiesで回してコード上簡潔に1つずつプロパティをセットしようと考えましたが、Xamarin上?ではGetPropertiesはアクセスできず、できませんでした。

  2. もう一つ上にViewModelを作成して反映させる

public class BindingData:BindableBase
    {
        OrgData _data;
        public OrgData Data
        {
            get
            {
                return _data;
            }
            set
            {
                SetProperty(ref _data, value);
            }
        }
    }

 
こちらもPropertyChangedがnullになりだめでした。

ご回答いただきたいこと

そのままですが、サーバからデータを取得してMVVMで表示したいと思ったときに、皆様はどのような方法をとられていますか?
項目数が多い場合も考慮して、一つ一つプロパティをセットすることは避けたいです。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

質問への追記・修正、ベストアンサー選択の依頼

  • P3PPP

    2016/10/30 19:42

    PCLの場合は `GetProperties` の代わりに `GetRuntimeProperties()` が支えます。

    キャンセル

回答 3

+3

ちょっとプログラムを簡略化しましたが、以下で動作しました。

<?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="XFApp1.Page1">
  <StackLayout>
    <Label Text="{Binding Data.ID}" />
    <Label Text="{Binding Data.Text}" />
    <Button x:Name="btn1" Text="GetData" />    
  </StackLayout>
</ContentPage>
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace XFApp1
{
    public partial class Page1 : ContentPage
    {
        private int _count;

        public Page1()
        {
            InitializeComponent();
            var _vm = new TestViewModel {Data = new DataModel()};
            BindingContext = _vm;
            btn1.Clicked += (sender, e) =>
            {
                try
                {
                    var data = new DataModel
                    {
                        ID = ++_count,
                        Text = $"{_count} 回目のクリック"
                    };
                    (BindingContext as TestViewModel).Data = data;
                }
                catch (Exception ex)
                {
                    DisplayAlert("error", ex.Message, "OK");
                }
            };
        }
    }

    public class TestViewModel : BindableBase
    {
        private DataModel _data;

        public DataModel Data
        {
            get { return _data; }
            set { SetProperty(ref _data, value); }
        }
    }

    public class DataModel
    {
        public int ID { get; set; }
        public string Text { get; set; }
    }

    public abstract class BindableBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Equals(storage, value))
                return false;

            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

考えてみたこと2のもう一つ上にViewModelを作るパターンでプログラムを行っています。
そしてそのViewModel構造に対応するためにXamlのBindingのPathをID -> Data.IDというような形に変更しています。
このサンプルではDataModelはBindableBaseを継承していませんが、BidableBaseを継承してプロパティ変更を通知するようにすれば、ID、Textの個別の変更にも対応できると思います。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

checkベストアンサー

0

INotifyPropertyChangedは、インスタンスに対して変更を検知するので、新たなインスタンスを生成してしまうと、検知の対象にはならないです(のはず...)。

そのため、上述T-T-T-T-T-T-T-Tさんの記載のように、インスタンスのプロパティ(今回だと_vm.Data)にデータを渡さなければなりません。
(その際は、DataプロパティにもSetPropertyをつけます。)
もし、現状定義しているViewModelをそのまま(CodeやMessageプロパティも含め)データをバインドしたいのであれば、もう一こクラスをラップしてあげると実現できます(できなそうでしたら言っていただければサンプル書きます..)。

サンプルとして、Dataプロパティのみを渡すパターンで(プラスMVVMということでCommandも使ってみるとテスタビリティも上がってよいかと)を書いておきます。ボタンクリックでTextを渡して何かを処理する処理をCommandで実装しています。

<?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="HelloWorld.Views.Page1">

  <StackLayout x:Name="stackMain">
    <Entry x:Name="txtData" />
    <Label Text="{Binding Data.ID}" />
    <Label Text="{Binding Data.Text}" />
    <Button x:Name="btn1" Text="GetData" Command="{Binding SomethingCommand}" CommandParameter="{Binding Data.Text}" />
  </StackLayout>

</ContentPage>
public partial class Page1 : ContentPage
{
    public Page1()
    {
        InitializeComponent();
        //サンプルに適当なデータを初期化...
        var vm = new JsonData<OrgData> { Code = 1, Message = "init!", Data = new OrgData() { ID = 99, Text = "text!!!" } };

        stackMain.BindingContext = vm;
    }
}
public class JsonData<T> : BindableBase 
    {
        private T _data;
        private int _count;

        public int Code { get; set; }

        public string Message { get; set; }


        public T Data
        {
            get { return _data; }
            set { SetProperty(ref _data, value); }
        }

        public JsonData()
        {
            SomethingCommand = new Command<string>(
                execute: (inputText) =>
                {
                    _count++; //サンプルとしてカウントをアップしています。
                    Data = GetData(_count, inputText);
                });
        }

        public ICommand SomethingCommand { get; }

        private static T GetData(int count, string text)
        {
            //この処理も動的に変更させたいのであれば、Page1クラスのViewModelをインスタンス化するタイミングでデリゲートを渡してあげればよいかと。

            //なんらかのデータ処理でjsonを取得したと仮定したとします(とりあえずjsonを取得したと仮定)
            var jsonStringFromPersistence = GetJsonString(count, text);

            //Deserialize
            return JsonConvert.DeserializeObject<T>(jsonStringFromPersistence);
        }

        private static string GetJsonString(int count, string text)
        {
            return JsonConvert.SerializeObject(new OrgData()
            {
                ID = count,
                Text = $"{text}**{count}"
            });
        }

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

0

try
{
    var ret = txtData.Text;
    _vm = JsonConvert.DeserializeObject<JsonData<OrgData>>(ret);
}
catch(Exception ex)
{
    DisplayAlert("error", ex.Message, "OK");
}

これを↓のように書き換えてみては?

try
{
    var ret = txtData.Text;
    var temp = JsonConvert.DeserializeObject<JsonData<OrgData>>(ret);
    _vm.Data = temp.Data;
}
catch(Exception ex)
{
    DisplayAlert("error", ex.Message, "OK");
}

ご自身でも言われているように _vm のインスタンスが再生成されてしまう事が原因です。
そのため、_vmインスタンスを生成しなおさず、dataだけを取ってしまえばいいかと。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

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

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

関連した質問

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