C#/WPF

[WPF] Dependency Injection(DI) IoC 를 이용한 Navigation

suniverse 2023. 8. 23. 11:59

✍ DependencyInjection 설치 

✍ MainWindow.xaml 삭제 후 

Models

ViewModels

Views 

폴더 생성

Views 폴더에 MainView.xaml 생성

ViewModels에 MainViewModel 생성 

App.xaml 에서 StartupUri 제거 

 

💻 App.xaml.cs

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using WpfDINavigation.ViewModels;
using WpfDINavigation.Views;

namespace WpfDINavigation
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public new static App Current => (App)Application.Current;

        private IServiceProvider ConfigureServies()
        {
            var services = new ServiceCollection();

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

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

            return services.BuildServiceProvider();
        }

        public App()
        {
            Services = ConfigureServies();

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

        public IServiceProvider Services { get; }
    }
}

✔ Application.Current는 자기 자신을 호출하는 속성 

Services.GetRequiredService<MainView>(); 이 구문이 실행되면 

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

MainView 생성자가 생성이 되고 뷰모델이 DataContext로 들어간다고 생각하면 이해하기 쉽다. 

 

 

✍ MVVM 패턴을 위해 ViewModelBase 생성 

 

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

 

✔ CallerMemberName을 사용하면 매개변수를 입력하지 않았을 때, 그 속성의 이름이 그대로 인자값으로 넘어온다. 

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

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

        // 속성값이 변경되었을 때 이벤트에 알려주기 위해 메서드 생성 
        protected void OnPropertyChanged([CallerMemberName]string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

 

✍ RelayCommand 생성

RelayCommand는 Command 바인딩을 위해 ICommand를 상속받는다. 

ICommand 안에는 canexecute와 execute가 있다. 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfDINavigation.Commands
{
    public class RelayCommand<T> : ICommand
    {
        private readonly Action<T>? _execete;
        private readonly Predicate<T>? _canExecute;

        public RelayCommand(Action<T>? execute, Predicate<T>? canExecute = null)
        {
            this._execete = execute;
            this._canExecute = canExecute;
        }

        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object? parameter)
        {
            return _canExecute?.Invoke((T)parameter) ?? true;
        }

        public void Execute(object? parameter)
        {
            _execete?.Invoke((T)parameter);
        }
    }
}

 

✔ CanExecute

커맨드를 실행할지에 대한 제한조건을 건다 

해당 콜백함수가 만약 false라면 커맨드를 실행할 수 없다. 

 

✔ Execute

커맨드를 실행하는 구문 

파라미터를 매개변수로 넘겨주고 콜백함수를 실행시킨다. 

 

 

💻 MainView.xaml

xmlns:viewmodels="clr-namespace:WpfDINavigation.ViewModels"

추가하기 

    <Window.Resources>
        <DataTemplate DataType="{x:Type viewmodels:LoginViewModel}">
            <local:LoginView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:SignupViewModel}">
            <local:LoginView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:TestViewModel}">
            <local:LoginView/>
        </DataTemplate>
    </Window.Resources>

    <Window.Resources>에 DataTemplate에 뷰모델을 추가해준다 

 

<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:SignupViewModel}">
            <local:SignupView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:TestViewModel}">
            <local:TestView/>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ContentControl Content="{Binding CurrentViewModel}"/>
    </Grid>
</Window>
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 = value; 
                    OnPropertyChanged();
                }
            }
        }
    }
}

--> CurrentViewModel 추가 

 

✔ LoginView, SignupView, TestView 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>
        <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}" Content="ToTest" Grid.Column="1"/>
        </Grid>
    </Grid>
</UserControl>
<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="Yellow">
        <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>
<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="SkyBlue">
        <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>


✍ 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
    {
		private INotifyPropertyChanged? _currentViewmodel;

		public INotifyPropertyChanged? CurrentViewModel
		{
			get { return _currentViewmodel; }
			set 
			{ 
				_currentViewmodel = value;
				CurrentViewModelChanged?.Invoke();
				_currentViewmodel = null;
			}
		}

		public Action? CurrentViewModelChanged { get; set; }

	}
}

 

✍ NavigationService 생성하기 

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

namespace WpfDINavigation.Services
{
    public class NavigationService : INavigationService
    {
        private readonly MainNavigationStore _mainNavigationStore;

        private INotifyPropertyChanged CurrentViewModel
        {
            set => _mainNavigationStore.CurrentViewModel = value;
        }

        public NavigationService(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;
            }
        }
    }
}

💻 INavigationService 인터페이스 

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

namespace WpfDINavigation.Services
{
    public interface INavigationService
    {
        void Navigate(NaviType naviType);
    }
}

💻 RelayCommands

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfDINavigation.Commands
{
    public class RelayCommand<T> : ICommand
    {
        private readonly Action<T>? _execete;
        private readonly Predicate<T>? _canExecute;

        public RelayCommand(Action<T>? execute, Predicate<T>? canExecute = null)
        {
            this._execete = execute;
            this._canExecute = canExecute;
        }

        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object? parameter)
        {
            return _canExecute?.Invoke((T)parameter) ?? true;
        }

        public void Execute(object? parameter)
        {
            _execete?.Invoke((T)parameter);
        }
    }
}

 

💻 LoginViewModel

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using WpfDINavigation.Commands;
using WpfDINavigation.Services;

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using WpfDINavigation.Commands;
using WpfDINavigation.Services;

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

💻 ViewModelBase

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

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

        // 속성값이 변경되었을 때 이벤트에 알려주기 위해 메서드 생성 
        protected void OnPropertyChanged([CallerMemberName]string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

 

💻 App.xaml.cs

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using WpfDINavigation.Services;
using WpfDINavigation.Stores;
using WpfDINavigation.ViewModels;
using WpfDINavigation.Views;

namespace WpfDINavigation
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public new static App Current => (App)Application.Current;

        private IServiceProvider ConfigureServies()
        {
            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 = ConfigureServies();

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

        public IServiceProvider Services { get; }
    }
}