자알못 자마린 – Xamarin.Forms Data Access(데이터 접근) 기초

Table of Content

Xamarin.Forms 애플리케이션의 데이터를 저장하는 방법

Application Properties 사용

Xamarin.Forms 애플리케이션은 Properties라는 속성을 가지고 있다. 이 속성은 IDictionary<string, object> 타입이므로 모든 객체를 저장할 수 있지만 애플리케이션의 설정 또는 일시적인 데이터를 저장하는 것이 좋다.

File System 사용

모든 장치의 애플리케이션에는 디렉토리가 할당되며 이곳에 무엇이든 저장할 수 있다.

SQLite 사용

가벼운 관계형 데이터베이스인 SQLite에 데이터를 저장할 수 있다.

Application Properties 사용하기

애플리케이션의 설정을 저장하는 경우는 다음 세 가지 정도 있을 것이다.

  • 설정 페이지의 [저장] 버튼을 누르면 변경된 설정을 저장하는 방식
  • 변경된 설정을 즉각적으로 저장하는 방식
  • 설정 페이지를 벗어나면 저장하는 방식

아래 예제는 위의 세 번째 경우인, 변경된 값을 즉시 Application.Properties 속성에 저장하는 설정 페이지 구현 예제다.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    xmlns:local="clr-namespace:MyFirstXamarinApp"
    xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.MainPage">
    <TableView Intent="Form">
        <TableRoot>
            <TableSection>
                <EntryCell x:Name="title" Text="{Binding Title}" Label="할일" Placeholder="(예: 쇼핑)" Completed="OnChange"/>
                <SwitchCell x:Name="notificationsEnabled" Text="Notifications" On="{Binding NotificationsEnabled}" OnChanged="OnChange" />
            </TableSection>
        </TableRoot>
    </TableView>
</ContentPage>

MainPage.xaml.cs

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class MainPage : ContentPage
    {
        // 생성자: Page를 띄우면 설정 복원
        public MainPage()
        {
            InitializeComponent();

            // 설정 적용
            if (Application.Current.Properties.ContainsKey("Name"))
                title.Text = Application.Current.Properties["Name"].ToString();
            if (Application.Current.Properties.ContainsKey("NotificationsEnabled"))
                notificationsEnabled.On = (bool)Application.Current.Properties["NotificationsEnabled"];
        }

        // EntryCell, SwitchCell 설정 변경 이벤트 처리기를 하나로 통함
        // object 및 System.EventArgs 타입 객체를 매개변수로 갖는 메소드는 모든 종류의 이벤트 처리기로 사용 가능
        void OnChange(object sender, System.EventArgs e)
        {
            Application.Current.Properties["Name"] = title.Text;
            Application.Current.Properties["NotificationsEnabled"] = notificationsEnabled.On;

            // 위의 부분까지 코드를 작성하면 즉시 저장되지 않음. 백그라운드 모드로 전환될 때 저장됨.
            // SavePropertiesAsync() 메소드를 사용하면 즉시 저장됨.
            Application.Current.SavePropertiesAsync();
        }

        // 사용자가 이 Page를 벗어났을 때의 행동
        protected override void OnDisappearing()
        {
            // Page를 벗어났을 때 설정을 저장하는 코드를 이곳에 작성해도 되지만
            // 이벤트 처리기에서 이미 구현했으므로 생략... 
            base.OnDisappearing();
        }
    }
}

실행화면

할일과 알림 설정 후 앱을 종료하고 다시 실행하면 그대로 나타나있다.

더 완벽한 Application Properties 구현 1 – C#

위 예제 코드는 두 가지 문제점이 있다. Properties에 사용할 키 값을 변경하면 해당 키 값을 사용하는 코드를 전부 수정해야 한다는 점과 설정 Page 외에 다른 Page에서 해당 설정값을 사용하지 못한다는 점이다.

이 문제를 해결하기 위해 App 클래스(App.xaml.cs)에 상수를 선언하여 사용한다.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    xmlns:local="clr-namespace:MyFirstXamarinApp"
    xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.MainPage">
    <TableView Intent="Form">
        <TableRoot>
            <TableSection>
                <EntryCell x:Name="title" Text="{Binding Title}" Label="할일" Placeholder="(예: 쇼핑)" Completed="OnChange"/>
                <SwitchCell x:Name="notificationsEnabled" Text="알림" On="{Binding NotificationsEnabled}" OnChanged="OnChange" />
            </TableSection>
        </TableRoot>
    </TableView>
</ContentPage>

App.xaml.cs

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace MyFirstXamarinApp
{
    public partial class App : Application
    {
        // 상수 선언
        private const string TitleKey = "Name";
        private const string NotificationsEnabledKey = "NotificationsEnabled";

        // 속성 선언
        public string Title
        {
            get
            {
                if (Properties.ContainsKey(TitleKey))
                    return Properties[TitleKey].ToString();
                return "";
            }

            set
            {
                Properties[TitleKey] = value;
            }
        }

        public bool NotificationsEnabled
        {
            get
            {
                if (Properties.ContainsKey(NotificationsEnabledKey))
                    return (bool)Properties[NotificationsEnabledKey];
                return false;
            }

            set
            {
                Properties[NotificationsEnabledKey] = value;
            }
        }

        public App()
        {
            InitializeComponent();

            MainPage = new NavigationPage(new MainPage());
        }

        protected override void OnStart()
        {
            // Handle when your app starts
        }

        protected override void OnSleep()
        {
            // Handle when your app sleeps
        }

        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }
}

MainPage.xaml.cs

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class MainPage : ContentPage
    {
        // 생성자: Page를 띄우면 설정 복원
        public MainPage()
        {
            InitializeComponent();

            var app = Application.Current as App;
            title.Text = app.Title;
            notificationsEnabled.On = app.NotificationsEnabled;
        }

        // EntryCell, SwitchCell 설정 변경 이벤트 처리기를 하나로 통함
        // object 및 System.EventArgs 타입 객체를 매개변수로 갖는 메소드는 모든 종류의 이벤트 처리기로 사용 가능
        void OnChange(object sender, System.EventArgs e)
        {
            var app = Application.Current as App;
            app.Title = title.Text;
            app.NotificationsEnabled = notificationsEnabled.On;
        }

        // 사용자가 이 Page를 벗어났을 때의 행동
        protected override void OnDisappearing()
        {
            // Page를 벗어났을 때 설정을 저장하는 코드를 이곳에 작성해도 되지만
            // 이벤트 처리기에서 이미 구현했으므로 생략... 
            base.OnDisappearing();
        }
    }
}

실행화면

생략… 위의 예제와 동일.

더 완벽한 Application Properties 구현 2 – XAML

XAML Data Binding 표현식을 사용하면 위의 예제 코드를 더 간결하게 작성할 수 있다.

MainPage.xaml

위의 예제와 달리 x:Name과 이벤트 처리기는 더 이상 필요없다.

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    xmlns:local="clr-namespace:MyFirstXamarinApp"
    xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.MainPage">
    <TableView Intent="Form">
        <TableRoot>
            <TableSection>
                <EntryCell Text="{Binding Title}" Label="할일" Placeholder="(예: 쇼핑)" />
                <SwitchCell Text="알림" On="{Binding NotificationsEnabled}" />
            </TableSection>
        </TableRoot>
    </TableView>
</ContentPage>

App.xaml.cs

생략… 위의 예제와 동일.

MainPage.xaml.cs

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class MainPage : ContentPage
    {
        // 생성자: Page를 띄우면 설정 복원
        public MainPage()
        {
            InitializeComponent();

            // XAML Data Binding 사용을 위한 BindingContext 속성
            BindingContext = Application.Current;
        }

        // 사용자가 이 Page를 벗어났을 때의 행동
        protected override void OnDisappearing()
        {
            base.OnDisappearing();
        }
    }
}

실행화면

생략… 위의 예제와 동일.

파일 시스템(File System) 사용하기

안드로이드 및 iOS는 파일 입출력에 System.IO를, Windows 8.1 이상은 Windows.Storage라는 새로운 파일 시스템 모델을 사용한다. 플랫폼별로 파일 시스템을 다르게 사용해야 한다면 이를 어떻게 사용해야 할까?

이를 해결하려면 인터페이스(Interface)를 사용하거나, 인터페이스를 사용하기 곤란하다면 PCLStorage를 사용하면 된다… 라고 하는데 PCLStorage는 더 이상 사용되지 않는 듯하다. 이 부분은 나중에…

PCLStorage

자세한 사용법은 Github – PCLStorage 사이트 참고…

SQLite 사용하기

아래에 설명할 SQLite 사용 방법은 다음과 같다.

  1. 모든 프로젝트에 NuGet 패키지(sqlite-net-pcl) 추가
  2. 각 플랫폼 별로 SQLite DB파일 생성 및 접근 코드 구현
  3. 공유 프로젝트에서 SQLite DB파일 제어

아래에 설명할 예제에서 Android와 iOS는 SQLite DB파일을 생성하고 접근하기 위해 System.IO를 사용하지만 UWP는 Windows.Storage를 사용한다. 따라서 플랫폼 별로 SQLite DB파일에 접근하는 방법을 다르게 구현해야 한다. 플랫폼 별로 코드를 다르게 작동하게 하려면 어떻게 해야 할까…?

DependencyService를 사용하면 각 플랫폼 별로 특성화된 코드를 작동하게 할 수 있다. 각 플랫폼에서 Dependency 어셈블리를 생성한 후 공유 프로젝트에서 DependencyService를 사용하면 된다. 자세한 설명은 아래 예제 코드를 참고…

아래 예제는 ListView에 Item을 추가하고 그 정보를 SQLite DB파일로 저장하는 예제이며, 아래 예제의 공유 프로젝트 명은 MyFirstXamarinApp이다.

모든 프로젝트에 sqlite-net-pcl 패키지 추가

공유 프로젝트, 안드로이드 프로젝트(.Android), iOS 프로젝트(.iOS), UWP 프로젝트(.UWP) 모두 sqlite-net-pcl 패키지를 추가한다.

프로젝트 별로 마우스 우클릭 -> 추가 -> NuGet 패키지 추가

인터페이스 생성

공유 프로젝트에 인터페이스를 생성한다. 이 인터페이스는 SQLite DB파일을 생성하고 접근하는 메소드를 하나 갖는다. 플랫폼 별로 파일 입출력 방식이 다르기 때문에 플랫폼 별로 DB 파일 접근 방식을 다르게 하기 위해 인터페이스를 사용한다.

MyFirstXamarinApp.ISQLiteDb.cs

using SQLite;

namespace MyFirstXamarinApp
{
    public interface ISQLiteDb
    {
        SQLiteAsyncConnection GetConnection();
    }
}

플랫폼 별로 SQLite DB파일 생성 및 접근 코드 구현

안드로이드 프로젝트(.Android), iOS 프로젝트(.iOS), UWP 프로젝트(.UWP) 각각에 SQLite DB파일을 생성하고 접근하기 위한 클래스와 메소드를 생성한다. 이 클래스는 위에서 생성한 인터페이스를 상속받는다.

각 플랫폼에서 서로 다른 방식으로 SQLite DB파일을 생성하고 접근하기 위해 Dependency 어셈블리를 생성한다. 이 어셈블리는 공유 프로젝트에서 DependencyService에 의해 사용된다.

MyFirstXamarinApp.Android.SQLiteDb.cs

using System;
using System.IO;
using SQLite;
using Xamarin.Forms;
using MyFirstXamarinApp.Droid;

[assembly: Dependency(typeof(SQLiteDb))]
namespace MyFirstXamarinApp.Droid
{
  public class SQLiteDb : ISQLiteDb
  {
    public SQLiteAsyncConnection GetConnection()
    {
      var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
      var path = Path.Combine(documentsPath, "MySQLite.db3");

      return new SQLiteAsyncConnection(path);
    }
  }
}

MyFirstXamarinApp.iOS.SQLiteDb.cs

using System;
using System.IO;
using SQLite;
using Xamarin.Forms;
using MyFirstXamarinApp.iOS;

[assembly: Dependency(typeof(SQLiteDb))]
namespace MyFirstXamarinApp.iOS
{
  public class SQLiteDb : ISQLiteDb
  {
    public SQLiteAsyncConnection GetConnection()
    {
      var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
      var path = Path.Combine(documentsPath, "MySQLite.db3");

      return new SQLiteAsyncConnection(path);
    }
  }
}

MyFirstXamarinApp.UWP.SQLiteDb.cs

UWP는 테스트해보지 못한 코드임…

using System;
using System.IO;
using SQLite;
using Xamarin.Forms;
using MyFirstXamarinApp.UWP;

[assembly: Dependency(typeof(SQLiteDb))]
namespace MyFirstXamarinApp.UWP
{
    public class SQLiteDb : ISQLiteDb
    {
        public SQLiteAsyncConnection GetConnection()
        {
      var documentsPath = ApplicationData.Current.LocalFolder.Path;
        	var path = Path.Combine(documentsPath, "MySQLite.db3");
        	return new SQLiteAsyncConnection(path);
        }
    }
}

공유 프로젝트에서 SQLite DB 파일 접근 및 제어

공유 프로젝트에 SQLite DB에 담길 테이블 정보를 갖는 클래스를 생성한다. 여기서는 Id와 Name을 Column으로 갖는 테이블을 생성한다.

DependencyService를 사용하여 어셈블리를 탐색한 후 플랫폼 별로 특성화된 코드를 작동하도록 한다.

MyFirstXamarinApp.MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    xmlns:local="clr-namespace:MyFirstXamarinApp"
    xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.MainPage">
    <StackLayout>
        <StackLayout Orientation="Horizontal">
            <Button Text="Add" Clicked="OnAdd" />
            <Button Text="Update" Clicked="OnUpdate" HorizontalOptions="CenterAndExpand" />
            <Button Text="Delete" Clicked="OnDelete" />
        </StackLayout>
        <ListView x:Name="recipesListView">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextCell Text="{Binding Name}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

MyFirstXamarinApp.MainPage.xaml.cs

using System;
using System.Collections.ObjectModel;
using SQLite;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    // SQLite DB에 담길 테이블 정보를 갖는 클래스
    [Table("Recipes)")] // SQLite에 정의된 애트리뷰트
    public class Recipe
    {
        [PrimaryKey, AutoIncrement, Column("RecipeId")]
        public int Id { get; set; }

        [MaxLength(255)]
        public string Name { get; set; }
    }

    public partial class MainPage : ContentPage
    {
        private SQLiteAsyncConnection _connection;
        private ObservableCollection<Recipe> _recipes; // ObservableCollection을 사용하면 ListView를 자동 갱신할 수 있음

        public MainPage()
        {
            InitializeComponent();

            // DependencyService를 통해 플랫폼 별로 고유한 방식으로 SQLite DB파일에 접근
            _connection = DependencyService.Get<ISQLiteDb>().GetConnection();
        }

        // OnAppearing(): 페이지가 나타날 때마다 호출
        protected override async void OnAppearing()
        {
            // 테이블 생성
            await _connection.CreateTableAsync<Recipe>(); // DB에 Recipe라는 테이블이 있다면 아무 일도 일어나지 않음

            // Recipe 테이블의 모든 리스트 가져오기
            var recipes = await _connection.Table<Recipe>().ToListAsync();

            // ObservableCollection 객체 생성
            _recipes = new ObservableCollection<Recipe>(recipes);

            // ListView에 보여줄 ObservableCollections 객체 설정
            recipesListView.ItemsSource = _recipes;

            base.OnAppearing();
        }

        async void OnAdd(object sender, System.EventArgs e)
        {
            // DB 및 ListView에 추가할 Item 생성
            var recipe = new Recipe { Name = "Recipe" + DateTime.Now.Ticks };

            // DB에 레코드 추가
            await _connection.InsertAsync(recipe);

            // ListView에 Item 추가
            _recipes.Add(recipe);
        }

        async void OnUpdate(object sender, System.EventArgs e)
        {
            try
            {
                // ListView Item 얻어오기([0]: 첫 번째 아이템)
                var recipe = _recipes[0];

                // DB에서 해당 레코드 갱신
                await _connection.UpdateAsync(recipe);

                // Item 갱신
                recipe.Name += " Updated.";
            }

            catch (Exception exception)
            {
                await DisplayAlert("Warning!", exception.ToString(), "OK");
            }

        }

        async void OnDelete(object sender, System.EventArgs e)
        {
            try
            {
                // DB 및 ListView에서 제거할 Item 얻어오기([0]: 첫 번째 아이템)
                var recipe = _recipes[0];

                // DB에서 해당 레코드 제거
                await _connection.DeleteAsync(recipe);

                // Item 제거
                _recipes.Remove(recipe);
            } 

            catch(Exception exception)
            {
                await DisplayAlert("Warning!", exception.ToString(), "OK");
            }
        }
    }
}

실행화면

INotifyPropertyChanged 인터페이스

위의 예제에서 ListView의 ItemSource 내용을 갱신하면 ListView에 바로 반영되지 않는다. 그 이유는 ObservableCollection 객체의 변경사항을 ListView가 인식하지 못하기 때문이다.

이 문제를 해결하려면 객체의 변경사항을 외부에 알려야 하는 클래스에 INotifyPropertyChanged 인터페이스를 구현하면 된다. 객체를 추가하거나 제거하면 이벤트가 발생하여 ListView가 이를 인식할 수 있게 된다.

구현 순서

  1. 변경사항을 알릴 프로퍼티가 속한 클래스에서 InotifyPropertyChagned 인터페이스 상속
  2. 변경사항을 알릴 프로퍼티의 setter에서 이벤트 처리기 호출

아래 예제는 위의 예제와 동일하며 MainPage.xaml.cs 파일만 수정한다.

MyFirstXamarinApp.MainPage.xaml.cs

using System;
using System.Collections.ObjectModel; // ObservableCollection
using System.ComponentModel; // INotifyPropertyChanged 
using System.Runtime.CompilerServices; //[CallerMemberName]
using SQLite;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    // SQLite DB에 담길 테이블 정보를 갖는 클래스
    [Table("Recipes)")] // SQLite에 정의된 애트리뷰트
    public class Recipe : INotifyPropertyChanged // 변경사항을 알릴 프로퍼티가 속한 클래스에서 INotifyPropertyChanged 인터페이스 상속
    {
        public event PropertyChangedEventHandler PropertyChanged; // 이벤트 처리기

        [PrimaryKey, AutoIncrement, Column("RecipeId")]
        public int Id { get; set; }

        // NotifyPropertyChanged 이벤트를 발생시키려면 자동 구현 프로퍼티(Auto-Implemented Properties)를 사용하면 안 됨
        private string _name;
        [MaxLength(255)]
        public string Name
        {
            get { return _name; }
            set
            {
                // 할당된 값이 현재 값과 다른 경우 이벤트 발생
                if (_name == value)
                    return;

                _name = value;

                // 이벤트 처리기 호출 방법 1: 직접 호출
                // C#의 널 조건 연산자: 조건?.null아닌경우
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }

        // 이벤트 처리기 호출 방법 2: 메소드를 별도로 만든 후 OnPropertyChanged(); 코드로 호출
        // [CallerMemberName] 애트리뷰트: 현재 메소드를 호출한 메소드 또는 프로퍼티의 이름을 자동 입력
        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public partial class MainPage : ContentPage
    {
        private SQLiteAsyncConnection _connection;
        private ObservableCollection<Recipe> _recipes; // ObservableCollection을 사용하면 ListView를 자동 갱신할 수 있음

        public MainPage()
        {
            InitializeComponent();

            // DependencyService를 통해 플랫폼 별로 고유한 방식으로 SQLite DB파일에 접근
            _connection = DependencyService.Get<ISQLiteDb>().GetConnection();
        }

        // OnAppearing(): 페이지가 나타날 때마다 호출
        protected override async void OnAppearing()
        {
            // 테이블 생성
            await _connection.CreateTableAsync<Recipe>(); // DB에 Recipe라는 테이블이 있다면 아무 일도 일어나지 않음

            // Recipe 테이블의 모든 리스트 가져오기
            var recipes = await _connection.Table<Recipe>().ToListAsync();

            // ObservableCollection 객체 생성
            _recipes = new ObservableCollection<Recipe>(recipes);

            // ListView에 보여줄 ObservableCollections 객체 설정
            recipesListView.ItemsSource = _recipes;

            base.OnAppearing();
        }

        // 
        async void OnAdd(object sender, System.EventArgs e)
        {
            // DB 및 ListView에 추가할 Item 생성
            var recipe = new Recipe { Name = "Recipe" + DateTime.Now.Ticks };

            // DB에 레코드 추가
            await _connection.InsertAsync(recipe);

            // ListView에 Item 추가
            _recipes.Add(recipe);
        }

        async void OnUpdate(object sender, System.EventArgs e)
        {
            try
            {
                // ListView Item 얻어오기([0]: 첫 번째 아이템)
                var recipe = _recipes[0];

                // DB에서 해당 레코드 갱신
                await _connection.UpdateAsync(recipe);

                // Item 갱신
                recipe.Name += " Updated.";
            }

            catch (Exception exception)
            {
                await DisplayAlert("Warning!", exception.ToString(), "OK");
            }

        }

        async void OnDelete(object sender, System.EventArgs e)
        {
            try
            {
                // DB 및 ListView에서 제거할 Item 얻어오기([0]: 첫 번째 아이템)
                var recipe = _recipes[0];

                // DB에서 해당 레코드 제거
                await _connection.DeleteAsync(recipe);

                // Item 제거
                _recipes.Remove(recipe);
            } 

            catch(Exception exception)
            {
                await DisplayAlert("Warning!", exception.ToString(), "OK");
            }
        }
    }
}

실행화면

RESTful Service

REST(Representational state transfer)는 범용적인 사용성을 보장하는 아키텍처 스타일이다. 쉽게 말해 클라이언트와 서버 사이에서 데이터를 주고받기 위한 표준이라고 보면 될듯 싶다.  이런 REST의 기본 규칙을 잘 지킨 서비스를 RESTful Service라고 부르며 이에 관한 자세한 설명은 이 사이트를 참고하기 바란다.

아래 예제는 서버로부터 JSON 데이터를 주고받으며 ListView의 Item을 추가, 수정 및 삭제하는 예제다. 실습을 위해 REST API를 제공하는 JSONPlaceholder를 사용했다.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    xmlns:local="clr-namespace:MyFirstXamarinApp"
    xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.MainPage">
    <StackLayout>
        <StackLayout Orientation="Horizontal">
            <Button Text="Add" Clicked="OnAdd" />
            <Button Text="Update" Clicked="OnUpdate" HorizontalOptions="CenterAndExpand" />
            <Button Text="Delete" Clicked="OnDelete" />
        </StackLayout>
        <ListView x:Name="postsListView">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextCell Text="{Binding Title}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

MainPage.xaml.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; // ObservableCollection
using System.ComponentModel; // INotifyPropertyChanged
using System.Net.Http; // HttpClient
using Newtonsoft.Json; // JsonConvert
using Xamarin.Forms;

// .iOS 프로젝트 빌드 에러 발생하는 경우 
// .iOS 프로젝트 옵션 -> iOS 빌드 -> HttpClient 구현이 'NSUriSession (iOS 7+)'로 되어있는지 확인 또는 TLS 구현을 mono로 변경

namespace MyFirstXamarinApp
{
    public class Post : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public int Id { get; set; }

        private string _title;
        public string Title
        {
            get { return _title; }
            set
            {
                if (_title == value)
                    return;
                _title = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
            }
        }

        public string Body { get; set; }
    }

    public partial class MainPage : ContentPage
    {
        private const string Url = "https://jsonplaceholder.typicode.com/posts";
        private HttpClient _client = new HttpClient();
        private ObservableCollection<Post> _posts;

        public MainPage()
        {
            InitializeComponent();
        }

        // OnAppearing(): 페이지가 나타날 때마다 호출
        protected override async void OnAppearing()
        {
            // 서버로부터 String 타입의 JSON 데이터를 얻어온 후(HTTP Get 요청) JSON 역직렬화
            var content = await _client.GetStringAsync(Url);
            var posts = JsonConvert.DeserializeObject<List<Post>>(content);

            // ListView에 ObservableCollection 적용
            _posts = new ObservableCollection<Post>(posts);
            postsListView.ItemsSource = _posts;

            base.OnAppearing();
        }

        async void OnAdd(object sender, System.EventArgs e)
        {
            var post = new Post { Title = "Title" + DateTime.Now.Ticks };
            _posts.Insert(0, post);

            // JSON 형식의 문자열로 직렬화한 후 서버로 업로드(HTTP Post 요청)
            var content = JsonConvert.SerializeObject(post);
            await _client.PostAsync(Url, new StringContent(content));

            // _posts.Insert(0, post); // 이 코드는 서버 응답이 돌아온 후에 실행됨
        }

        async void OnUpdate(object sender, System.EventArgs e)
        {
            var post = _posts[0];
            post.Title += " UPDATED.";

            // JSON 형식의 문자열로 직렬화한 후 서버로 수정 요청(HTTP Put 요청)
            var content = JsonConvert.SerializeObject(post);
            await _client.PutAsync(Url + "/" + post.Id, new StringContent(content));
        }

        async void OnDelete(object sender, System.EventArgs e)
        {
            var post = _posts[0];
            _posts.Remove(post);

            // 서버로 삭제 요청(HTTP Delete 요청)
            await _client.DeleteAsync(Url + "/" + post.Id);
        }
    }
}

실행화면


댓글 남기기