LAB/C#

MVVM 패턴 - Accuweather API 기반 날씨 검색 APP

it-lab-0130 2024. 12. 2. 20:35

 

MVVM 정의

Model (모델) : 데이터를 포함하는 데이터 구조

- 데이터 저장소와 직접적으로 연계되는 구조

- API 호출이 이뤄지는 부분

- DB 접근, 데이터와 관련된 연산

 

View (뷰) : 사용자에게 데이터를 표시하고 상호작용하는 UI

- 일반적으로 컨트롤러로 구성

- XAML에서 객체로 사용되는 부분

 

ViewModel (뷰 모델) : 모델에 값을 할당 하고 뷰와 상호작용 할 때 모델을 업데이트하는 역할

- 뷰모델 과 뷰는 데이터 바인딩 기능으로 연결

- 모델에서 선언한 클래스 객체 생성

- 객체가 생성 후 프로퍼티 값 할당

- 외부API, DB등에서 각 프로퍼티로 업데이트 되었을 때 값이 할당되는 동작 구현

 

** API Json 파일 C#클래스 모델로 변환 

https://jsonutils.com/

 

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
                 }
             }
         };
     }
}
 

 

[최종 WeatherVM.cs]
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));
 }