MVVM 패턴 - Accuweather API 기반 날씨 검색 APP
MVVM 정의
Model (모델) : 데이터를 포함하는 데이터 구조
- 데이터 저장소와 직접적으로 연계되는 구조
- API 호출이 이뤄지는 부분
- DB 접근, 데이터와 관련된 연산
View (뷰) : 사용자에게 데이터를 표시하고 상호작용하는 UI
- 일반적으로 컨트롤러로 구성
- XAML에서 객체로 사용되는 부분
ViewModel (뷰 모델) : 모델에 값을 할당 하고 뷰와 상호작용 할 때 모델을 업데이트하는 역할
- 뷰모델 과 뷰는 데이터 바인딩 기능으로 연결
- 모델에서 선언한 클래스 객체 생성
- 객체가 생성 후 프로퍼티 값 할당
- 외부API, DB등에서 각 프로퍼티로 업데이트 되었을 때 값이 할당되는 동작 구현
** API Json 파일 C#클래스 모델로 변환
AccuWeather API 사용한 날씨 검색 APP
Model 파일 생성
출처 입력
1. City.cs
2. Weather.cs
[City.cs]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WeatherApp.Model
{
// AdministrativeArea 와 중복되는 구조이기 때문에 주석 처리
//public class Country
//{
// public string ID { get; set; }
// public string LocalizedName { get; set; }
//}
public class Area
{
public string ID { get; set; }
public string LocalizedName { get; set; }
}
public class City
{
public int Version { get; set; }
public string Key { get; set; }
public string Type { get; set; }
public int Rank { get; set; }
public string LocalizedName { get; set; }
public Area Country { get; set; }
public Area AdministrativeArea { get; set; }
}
}
[Weather.cs]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WeatherApp.Model
{
// Imperial 과 중복된 구조
//public class Metric
//{
// public double Value { get; set; }
// public string Unit { get; set; }
// public int UnitType { get; set; }
//}
public class Units
{
public int Value { get; set; }
public string Unit { get; set; }
public int UnitType { get; set; }
}
public class Temperature
{
public Units Metric { get; set; }
public Units Imperial { get; set; }
}
public class CurrentConditions
{
public DateTime LocalObservationDateTime { get; set; }
public int EpochTime { get; set; }
public string WeatherText { get; set; }
public int WeatherIcon { get; set; }
public bool HasPrecipitation { get; set; }
public object PrecipitationType { get; set; }
public bool IsDayTime { get; set; }
public Temperature Temperature { get; set; }
public string MobileLink { get; set; }
public string Link { get; set; }
}
}
ViewModel 생성
출처 입력
** Json 사용을 위한 외부 라이브러리 설치
Manage Nuget Package -> Newtonsoft 라이브러리 설치
1. Helpers 폴더 생성 후 AccuWeatherHelper 클래스 생성 (API Request & Response 구현)
- API 요청하고 돌려받는 로직만 존재
- API로 도시 데이터 가져오기 (도시코드를 받기위해 사용)
- API로 현재 날씨 정보 데이터 가져오기
- API 에서 JSON으로 받아온 데이터를 String 으로 변환후 List에 저장
url로 API 요청을 request하고, response 값을 CurrentCondition 리스트에 저장
- 리턴값으로 넘김
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using WeatherApp.Model;
namespace WeatherApp.ViewModel.Helpers
{
// API 요청하고 돌려받는 로직만 존재
class AccuWeatherHelper
{
public const string API_KEY = "6hVoJ5ptlpMaYuRnHuFxJP4itoekiP8Z";
public const string BASE_URL = "http://dataservice.accuweather.com/";
public const string LANGUAGE = "ko-kr";
public const string AUTOCOMPLETE_ENDPOINT =
"locations/v1/cities/autocomplete?apikey={0}&q={1}&language={2}";
public const string CURRENT_CONDITIONS_ENDPOINT =
"currentconditions/v1/{0}?apikey={1}&language={2}";
// 도시 데이터 가져오기
public static async Task<List<City>> GetCities(string query)
{
// API Json 결과에서 City 객체 타입 데이터를 List에 담습니다.
List<City> cities = new List<City>();
// 중요 우리가 가져다 쓰지만 어디를 고쳐야하는지 알아야됨
// API 요청 URL 을 설정합니다.
string url = BASE_URL + string.Format(AUTOCOMPLETE_ENDPOINT, API_KEY, query, LANGUAGE);
// HttpClient 에서
using (HttpClient clnt = new HttpClient())
{
// Json을 문자열로 바꾸고 문자열을 cities에 값을 넣음 클래스 모델형태로 가져오기 위해
// url로 API 요청을 request 하고, reponse 값을 cities 리스트에 할당합니다.
var res = await clnt.GetAsync(url);
string json = await res.Content.ReadAsStringAsync();
cities = JsonConvert.DeserializeObject<List<City>>(json);
}
return cities;
}
// 현재 날씨 정보 데이터 가져오기
public static async Task<CurrentConditions> GetCurrentConditions(string citikey)
{
// API Json 결과에서 CurrentConditions 객체 타입 데이터를 List에 담습니다.
CurrentConditions currentConditions = new CurrentConditions();
// API 요청 URL 을 설정합니다.
string url = BASE_URL + string.Format(CURRENT_CONDITIONS_ENDPOINT, citikey, API_KEY, LANGUAGE);
// HttpClient 에서
using (HttpClient clnt = new HttpClient())
{
// url로 API 요청을 request 하고, reponse 값을 CurrentConditions 리스트에 할당합니다.
var res = await clnt.GetAsync(url);
string json = await res.Content.ReadAsStringAsync();
currentConditions = JsonConvert.DeserializeObject<List<CurrentConditions>>(json).FirstOrDefault();
}
return currentConditions;
}
}
}
View 생성
출처 입력
1. View 폴더 생성
1-1. ViewModel 폴더에 WeatherVM.cs 파일 생성
** INotifyPropertyChanged 인터페이스
- 속성 값이 변경되었음을 클라이언트에 알립니다.
- 인터페이스 사용하면 바인딩을 다시 설정할 필요없이 바인딩 된 컨틀롱이 데이터 원본의 변경사항을 반영한다.
public class WeatherVM : INotifyPropertyChanged
{
// WeatherVM 생성자에서 , City.LocalizedName 값과
// CurrentConditions.WeatherText, CurrentConditions.Temperature.Unit 값을
// 할당한 상태로 시작한다.
// 값을 할당하는 동작은, 외부API, DB등에서 각 프로퍼티로 업데이트 되었을 때
// 값이 할당되는 동작과 동일하다.
// 데이터 바인딩 구조
// 1. 프로퍼티로 할당되고,
// 2. WeatherVM의 프로퍼티 변경이 감지되면,
// 3. 변경된 프로퍼티와 바인딩 된 모든 XAML 컨트롤에 반영한다.
public WeatherVM()
{
// ViewModel, WeatherVM 클래스의 생성자에서, CurrentConditions, SelectedCity 프로퍼티 초기화
Selectedcity = new City
{
LocalizedName = "서울"
};
CurrentConditions = new CurrentConditions
{
WeatherText = "Partly cloudy",
Temperature = new Temperature
{
Metric = new Units
{
Value = 21
}
}
};
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WeatherApp.Model;
namespace WeatherApp.ViewModel
{
public class WeatherVM : INotifyPropertyChanged
{
// WeatherVM 생성자에서 , City.LocalizedName 값과
// CurrentConditions.WeatherText, CurrentConditions.Temperature.Unit 값을
// 할당한 상태로 시작한다.
// 값을 할당하는 동작은, 외부API, DB등에서 각 프로퍼티로 업데이트 되었을 때
// 값이 할당되는 동작과 동일하다.
// 데이터 바인딩 구조
// 1. 프로퍼티로 할당되고,
// 2. WeatherVM의 프로퍼티 변경이 감지되면,
// 3. 변경된 프로퍼티와 바인딩 된 모든 XAML 컨트롤에 반영한다.
public WeatherVM()
{
// ViewModel, WeatherVM 클래스의 생성자에서, CurrentConditions, SelectedCity 프로퍼티 초기화
Selectedcity = new City
{
LocalizedName = "서울"
};
CurrentConditions = new CurrentConditions
{
WeatherText = "Partly cloudy",
Temperature = new Temperature
{
Metric = new Units
{
Value = 21
}
}
};
}
// [1] 시나리오 : 도시 검색을 요청(query)한다.
// query 객체 생성
private string query;
public string Query
{
get { return query; }
set
{
// [2] Property가 변경되면,
query = value;
// OnpropertyChanged 함수에서
OnPropertyChanged(nameof(Query));
}
}
// [1] 시나리오 : CurrentConditions 현재 날씨 정보가 업데이트 된다.
// currentCondition 객체 생성
private CurrentConditions currentCondition;
public CurrentConditions CurrentConditions
{
get { return currentCondition; }
set
{
// [2] Property가 변경되면,
currentCondition = value;
//OnPropertyChanged 함수에서
OnPropertyChanged(nameof(Query));
}
}
// [1] 시나리오 : City 정보가 업데이트 된다.
// selectedcity 객체 생성
private City selectedcity;
public City Selectedcity
{
get { return Selectedcity; }
set
{
// [2] Property가 변경되면,
Selectedcity = value;
// OnPropertyChanged 함수에서
OnPropertyChanged(nameof(Selectedcity));
}
}
// [4] 이벤트 핸들러가 동작하면,
// XAML에 바인딩 되어있는 모든 UI컨트롤에 해당 query 값이 업데이트 된다.
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
// [3] PropertyChanged 이벤트를 실행한다.
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
2. Weather.xaml 창 생성
2-0. App.xaml에 StartupUri 설정 : StartupUri = "View/Weather.xaml"
<Application x:Class="WeatherApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WeatherApp"
StartupUri="View/Weather.xaml">
<Application.Resources>
2-1. <Window> 태그에 네임 스페이스 등록 xmls:vm : ""
<Window x:Class="WeatherApp.View.Weather"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WeatherApp.View"
mc:Ignorable="d"
xmlns:vm="clr-namespace:WeatherApp.ViewModel"
Title="WeatherWindow" Height="600" Width="400">
2-2. <Window.Resources> 태그에 리소스 등록 ViewModel를 사용하기 위해서
<Window.Resources>
<vm:WeatherVM x:Key="vm"/>
</Window.Resources>
2-3. ViewModel Key값 선언 : 컨트롤러와 데이터간의 바인딩 적용하기 위해 사용
(객체선언) DataContext = {StaticResource vm}"
<Grid DataContext="{StaticResource vm}">
2-4. WeatherVM의 Query속성과 바인딩 : 입력값 Query로 바인딩
<TextBox Text="{Binding Query, Mode=TwoWay}"/>
2-5. 현재 날씨 정보 환경설정
- ViewModel, WeatherVM 클래스의 생성자에서, CurrentConditions 프로퍼티를 초기화한다.
- ViewModel(WeatherVM)에서 CurrentConditions 객체 선언 후 프로퍼티 설정
[View -> WeatherVM.cs]
// [1] 시나리오 : CurrentConditions 현재 날씨 정보가 업데이트 된다.
// currentCondition 객체 생성
private CurrentConditions currentCondition;
public CurrentConditions CurrentConditions
{
get { return currentCondition; }
set
{
// [2] Property가 변경되면,
currentCondition = value;
//OnPropertyChanged 함수에서
OnPropertyChanged(nameof(Query));
}
}
- Weather.xaml에서 API 받아올 데이터 DataContext 설정
<Grid Grid.Row="1"
Background="#4392f1"
DataContext="{Binding CurrentConditions}">
<!-- WeatherVM CurrentConditions 의 WeatherText 프로퍼티와 바인딩 -->
<TextBlock Text="{Binding WeatherText, Mode=TwoWay}"
Foreground="#f4f4f8"
FontSize="18"
Margin="20,0"/>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Temperature.Metric.Value, StringFormat={}{0}°C}"
Foreground="#f4f4f8"
FontSize="30"
Margin="20,0"/>
3. WeatherVM에 Selectedcity 객체 생성과 프로퍼티 할당
- ViewModel, WeatherVM 클래스의 생성자에서 SelectedCity 프로퍼티를 초기화한다.
// [1] 시나리오 : City 정보가 업데이트 된다.
// selectedcity 객체 생성
private City selectedcity;
public City Selectedcity
{
get { return Selectedcity; }
set
{
// [2] Property가 변경되면,
Selectedcity = value;
// OnPropertyChanged 함수에서
OnPropertyChanged(nameof(Selectedcity));
}
}
<!-- 4. WeatherVM의 Query 속성과 바인딩된 다른 컨트롤에 다 반영
+ TextBox 도시 입력 값과 연결되어 있음
Text = {Binding Query , Mode=TwoWay} -->
<TextBlock DataContext="{StaticResource vm}"
Text="{Binding Selectedcity.LocalizedName, Mode=TwoWay}"
Foreground="#f4f4f8"
FontSize="20"
Margin="20,0"/>
4. WeatherVM의 프로퍼티 변경이 감지되면, 변경된 프로퍼티와 바인딩된 모든 XAML 컨트롤에 반영
// [4] 이벤트 핸들러가 동작하면,
// XAML에 바인딩 되어있는 모든 UI컨트롤에 해당 query 값이 업데이트 된다.
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
// [3] PropertyChanged 이벤트를 실행한다.
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}