[C#프로그래밍] [WPF] PC 카카오톡 만들기 #11 - Dependency Injection(DI) IoC 를 이용한 Navigation (의존성 주입을 통한 네이게이션)

728x90

 

https://www.youtube.com/watch?v=KWZ3bxBe1mQ&list=PLlrfTSXS0LLKHfOfwM31jJw3SHuDnkF49&index=12

 

 📌 초기 세팅

- WpfDINavigation 프로젝트 생성 후 Nuget패키지 추가 및 MainWindow는 삭제 후 Models, ViewModels, Views 폴더 생성 

 

 📌 Views, ViewModels에 파일 추가 

 

- App.xaml에서 StartupUri 삭제 

<Application x:Class="WpfDINavigation.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml

 

-App.xaml.cs

namespace WpfDINavigation
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        //자기 자신을 호출 Current은 타입이 Application이기 때문에 App 타입으로 으로 변환
        public new static App Current => (App)Application.Current;

        //의존성 주입을 위한 생성자 규칙을 서비스에 등록하고 그 서비스를 반환해서 바깥에서 그 서비스를 실행해서
        //의존성 주입 가능하도록 하는 IServiceProvider 인터페이스 생성
        private IServiceProvider? ConfigureServices()
        {
            var service = new ServiceCollection();

            // ViewModels
            service.AddSingleton<MainViewModel>();  

            // VIews
            
            service.AddSingleton(s => new MainView()
            {
                //MainViewModel생성자 주입이 DataContext로 된다. 
                DataContext = s.GetRequiredService<MainViewModel>()
            });

            //BuildServiceProvider를 반환
            return service.BuildServiceProvider();
        }
        public App()
        {

            //생성자 (BuildServiceProvider) 로 생성자 주입
            Service = ConfigureServices();
            var mainView = Service.GetRequiredService<MainView>();
            mainView.Show();
        }


        public IServiceProvider Service { get;  }
    }
}

 

App()의 Service.GetRequiredService<MainView>(); 가 실행되면 private IServiceProvider? ConfigureServices()의 s => new MainView() 에서 MainView 생성자가 생성되고, DataContext = s.GetRequiredService<MainViewModel>() 에서 MainViewModel이 DataContext 로 들어간다.

 

📌 ViewModelBase  파일 추가 

- ViewModels 폴더에 ViewModelBase 파일 추가 

-ViewModelBase.cs

namespace WpfDINavigation.ViewModels
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        //속성값이 변화가 있을때 이벤트에 알려주기위해 함수 생성 
        //CallerMemberName 어트리뷰트를 사용하는 경우 OnPropertyChanged 를 호출했을 때 매개변수를 입력하지 않으면
        //그 속성의 이름이 인자값으로 넘어오게 된다.
        protected void OnPropertyChanged([CallerMemberName]string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

INotifyPropertyChanged 인터페이스는 MVVM 패턴의 필수 인터페이스

 

 📌 RelayCommand 추가

- Command Binding을 위해 추가 

- RelayCommand.cs

namespace WpfDINavigation.Commands
{
    // RelayCommand는 Command를 바인딩하기 위해 ICommand 사용
    //ICommand 에는 canExecute 와 execute 가 존재
    // canExecute : Command를 실행할지에 대한 제한 조건을 거는 것 
    // execute : :Command를 실행 
    public class RelayCommand<T> : ICommand
    {
        private readonly Action<T> _execute;
        private readonly Predicate<T>? _canExecute;
        public RelayCommand(Action<T>? execute, Predicate<T>? canExecute = null) 
        {
            this._execute = execute;
            this._canExecute = canExecute;
        }
        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }


        public bool CanExecute(object? parameter)
        {
            // 해당 콜백 함수가 false 를 반환하면 Command는 실행 불가  
            return _canExecute?.Invoke((T)parameter!) ?? true;
        }

        public void Execute(object? parameter)
        {
            //parameter를 매개변수로 넘겨주고 콜백함수 실행 
            _execute?.Invoke((T)parameter!);
        }
    }

}

 

 📌 View, ViewModel  생성

- 사용자 정의 컨트롤 LoginView, SingupView, TestView 추가 

 

- 클래스 LoginViewModel , SingupViewModel , TestViewModel  추가

 

 📌 MainView 디자인

- MainView.xaml

<Window x:Class="WpfDINavigation.Views.MainView"
        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:WpfDINavigation.Views"
        xmlns:viewmodels="clr-namespace:WpfDINavigation.ViewModels"
        mc:Ignorable="d"
        Title="MainView" Height="450" Width="800">

    <Window.Resources>
        <DataTemplate DataType="{x:Type viewmodels:LoginViewModel}">
            <local:LoginView/>
        </DataTemplate>
        
        <DataTemplate DataType="{x:Type viewmodels:SingupViewModel}">
            <local:SingupView/>
        </DataTemplate>
        
        <DataTemplate DataType="{x:Type viewmodels:TestViewModel}">
            <local:TestView/>
        </DataTemplate>
    </Window.Resources>
    
    <Grid>
        <ContentControl Content="{Binding CurrentViewModel}"/>
    </Grid>
</Window>

 

-MainViewModel.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfDINavigation.ViewModels
{
    public class MainViewModel : ViewModelBase
    {

        private INotifyPropertyChanged? _currentViewModel;

        public MainViewModel()
        {
            CurrentViewModel = new LoginViewModel();
        }


        public INotifyPropertyChanged? CurrentViewModel
        {
            get { return _currentViewModel; }
            set
            {

                if (_currentViewModel != value)
                {
                    //_currentViewModel 추가
                    _currentViewModel = value;

                    //OnPropertyChanged 호출
                    OnPropertyChanged();
                }

            }
        }

    }
}

 

  Binding 잘 작동하는지 확인 하기 

 

-LoginViewModel.cs

	public class LoginViewModel : ViewModelBase
	{
		private INotifyPropertyChanged? _currentViewModel;

		public MainViewModel()
		{
			CurrentViewModel = new LoginViewModel();
		}

 

- LoginViewModel.cs

namespace WpfDINavigation.ViewModels
{
	public class LoginViewModel : ViewModelBase
	{


	}
}

 

- LoginView.xaml

<UserControl x:Class="WpfDINavigation.Views.LoginView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfDINavigation.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    
    <Grid Background="Red">
            
    </Grid>
</UserControl>

 

 

 📌 사용자 정의 컨트롤 디자인

  LoginView.xaml

<UserControl x:Class="WpfDINavigation.Views.LoginView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfDINavigation.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    
    <Grid Background="Yellow">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <TextBlock Text="LoginView"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   FontSize="35"/>

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <Button Command="{Binding ToSignupCommand}" 
                    Content="ToSignup" 
                    Margin="5"/>

            <Button Command="{Binding ToTestCommand}"
                    Grid.Column="1" 
                    Content="ToTest" 
                    Margin="5"/>
        </Grid>
    </Grid>
</UserControl>

 

  SignupView.xaml

<UserControl x:Class="WpfDINavigation.Views.SignupView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfDINavigation.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid Background="pink">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <TextBlock Text="SignupView"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   FontSize="35"/>

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <Button Command="{Binding ToLoginCommand}" 
                    Content="ToLogin" 
                    Margin="5"/>

            <Button Command="{Binding ToTestCommand}"
                    Grid.Column="1" 
                    Content="ToTest" 
                    Margin="5"/>
        </Grid>
    </Grid>
</UserControl>

 

  TestView.xaml

<UserControl x:Class="WpfDINavigation.Views.TestView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfDINavigation.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid Background="LightSkyBlue">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <TextBlock Text="TestView"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   FontSize="35"/>

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <Button Command="{Binding ToSignupCommand}" 
                    Content="ToSignup" 
                    Margin="5"/>

            <Button Command="{Binding ToLoginCommand}"
                    Grid.Column="1" 
                    Content="ToLogin" 
                    Margin="5"/>
        </Grid>
    </Grid>
</UserControl>

 

 📌 Navigationg

 

  MainNavigationStore 생성 

- 네비게이팅을 위해서는 CurrentViewModel을 변경해야한다. 
  이를 위해 Store 폴더를 추가하여 MainNavigationStore 클래스파일 생성 

- MainNavigationStore.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfDINavigation.ViewModels;

namespace WpfDINavigation.Stores
{
    public class MainNavigationStore : ViewModelBase
    {
        //mainview에 속성 추가
        private INotifyPropertyChanged? _currentViewModel;

        public INotifyPropertyChanged? CurrentViewModel
        {
            get { return _currentViewModel; }
            set { 
                _currentViewModel = value;
                //메인뷰에서 실행시키면 CurrentViewModel를 
                //NameView에 있는 CurrentViewModel에 할당
                CurrentViewModelChanged ?.Invoke();
                _currentViewModel = null;

            }
        }

        //대리자 추가
        public Action CurrentViewModelChanged { get; set; }
    }
}

 

  MainNavigationService 생성

- Services 폴더 생성 후 클래스 NavigationService 및 인터페이스 INavigationService , 클래스 NaviType 파일 생성 

- NaviType.cs

namespace WpfDINavigation.Services
{
    public enum NaviType
    {
        LoginView, SignupView, TestView
    }
}

 

-INavigationService.cs

namespace WpfDINavigation.Services
{
    public interface INavigationService
    {

        void Navigate(NaviType naviType)
        {

        }
    }
}

 

-NavigationService.cs

namespace WpfDINavigation.Services
{
    public class NavigationService
    {
    }
}

이 까지 하면 타입까지 생성 완료한 것임

 

-NavigationService.cs

namespace WpfDINavigation.Services
{
    public class NavigationService : INavigationService
    {
        //의존성 주입 사용

        private readonly MainNavigationStore _mainNavigationStore;

        //_mainNavigationStore currentVireModel을 할당하기 위한 속성 추가
        private INotifyPropertyChanged CurrentViewModel
        {
            set => _mainNavigationStore.CurrentViewModel = value;
        }
        public NavigationService(MainNavigationStore mainNavigationStore)
        {
            //mainNavigationStore가 생성자의 매개변수로 들어오게 되면 
            //mainNavigationStore는 객체가 생성되어 넘어오게된다.

            //필드로 할당 
            this._mainNavigationStore = mainNavigationStore;
        }
        //인터페이스 구현
        public void Navigate(NaviType naviType)
        {
            switch (naviType)
            {
                case NaviType.LoginView:
                    //생성자주입
                    CurrentViewModel = (ViewModelBase)App.Current.Services.GetService(typeof(LoginViewModel))!;
                    break;                
                case NaviType.SignupView:
                    CurrentViewModel = (ViewModelBase)App.Current.Services.GetService(typeof(SignupViewModel))!;
                    break;               
                case NaviType.TestView:
                    CurrentViewModel = (ViewModelBase)App.Current.Services.GetService(typeof(TestViewModel))!;
                    break;
                default: return;

            }
        }
    }
}

 

-MainViewModel.cs

    public class MainViewModel : ViewModelBase
    {

        private INotifyPropertyChanged? _currentViewModel;
        private readonly MainNavigationStore _mainNavigationStore;


        private void CurrentViewModelChanged()
        {
            CurrentViewModel = _mainNavigationStore.CurrentViewModel;
        }
        public MainViewModel(MainNavigationStore mainNavigationStore)
        {
            _mainNavigationStore = mainNavigationStore;
            _mainNavigationStore.CurrentViewModelChanged += CurrentViewModelChanged;
        }

 

  ViewModel에 command 기능 추가

-LoginViewModel.cs

namespace WpfDINavigation.ViewModels
{
	public class LoginViewModel : ViewModelBase
	{
        private readonly INavigationService _navigationService;
        private void ToSignup(object _)
        {
            _navigationService.Navigate(NaviType.SignupView);
        }
        private void ToTest(object _)
        {
            _navigationService.Navigate(NaviType.TestView);
        }

		public LoginViewModel(INavigationService navigationService) 
		{
            _navigationService = navigationService;
            ToSignupCommand = new RelayCommand<object>(ToSignup);
			ToTestCommand = new RelayCommand<object>(ToTest);
        }

        public ICommand ToSignupCommand { get; set; }
        public ICommand ToTestCommand { get; set; }

    }
}

 

-SignupViewModel.cs

namespace WpfDINavigation.ViewModels
{
   public class SignupViewModel : ViewModelBase
    {
        private readonly INavigationService _navigationService;
        private void ToLogin(object _)
        {
            _navigationService.Navigate(NaviType.LoginView);
        }
        private void ToTest(object _)
        {
            _navigationService.Navigate(NaviType.TestView);
        }

        public SignupViewModel(INavigationService navigationService)
        {
            _navigationService = navigationService;
            ToLoginCommand = new RelayCommand<object>(ToLogin);
            ToTestCommand = new RelayCommand<object>(ToTest);
        }

        public ICommand ToLoginCommand { get; set; }
        public ICommand ToTestCommand { get; set; }

    }
}

 

 

 

  Dependency Injection Service 추가

 

-MainViewModel.cs

namespace WpfDINavigation.ViewModels
{
    public class MainViewModel : ViewModelBase
    {

        private INotifyPropertyChanged? _currentViewModel;
        private readonly MainNavigationStore _mainNavigationStore;


        private void CurrentViewModelChanged()
        {
            CurrentViewModel = _mainNavigationStore.CurrentViewModel;
        }
        public MainViewModel(MainNavigationStore mainNavigationStore)
        {
            _mainNavigationStore = mainNavigationStore;
            _mainNavigationStore.CurrentViewModelChanged += CurrentViewModelChanged;
        }


        public INotifyPropertyChanged? CurrentViewModel
        {
            get { return _currentViewModel; }
            set
            {

                if (_currentViewModel != value)
                {
                    //_currentViewModel 추가
                    _currentViewModel = value;

                    //OnPropertyChanged 호출
                    OnPropertyChanged();
                }

            }
        }

    }
}

 

-App.xaml.cs

namespace WpfDINavigation
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        //자기 자신을 호출 Current은 타입이 Application이기 때문에 App 타입으로 으로 변환
            public new static App Current => (App)Application.Current;
            private IServiceProvider ConfigureServices()
            {
                var services = new ServiceCollection();

                // Stores
                services.AddSingleton<MainNavigationStore>();

                // Services
                services.AddSingleton<INavigationService, NavigationService>();

                /// ViewModels
                services.AddSingleton<MainViewModel>();
                services.AddSingleton<LoginViewModel>();
                services.AddSingleton<SignupViewModel>();
                services.AddSingleton<TestViewModel>();

                // Views
                services.AddSingleton(s => new MainView()
                {
                    DataContext = s.GetRequiredService<MainViewModel>()
                });

                return services.BuildServiceProvider();
            }

            public App()
            {
                Services = ConfigureServices();

                var mainView = Services.GetRequiredService<MainView>();
                mainView.Show();
            }

            public IServiceProvider Services { get; }
    }
}

 

-MainViewModel.cs

        public MainViewModel(MainNavigationStore mainNavigationStore, INavigationService navigationService)
        {
            _mainNavigationStore = mainNavigationStore;
            _mainNavigationStore.CurrentViewModelChanged += CurrentViewModelChanged;
            navigationService.Navigate(NaviType.LoginView);
        }

여기까지 한 후 실행 해 보면 LoginView 가 화면에 정상적으로 보인다. ToSignup 을 클릭하면 ToSignup으로 이동 한다 ( ViewModelBase 상속처리 해줘야함)

 

 

728x90