티스토리 뷰

반응형

2022.02.21 - [WPF] - Microsoft.Toolkit.Mvvm을 이용한 간단한 프레임워크 part1

 

이번 포스팅에서는 Frame에서 Navigiation하는 방법에 대해서 설명하고 코드를 작성해 보도록 하겠습니다.

1. FrameBehavior

ViewModel에서 Frame를 직접 컨트롤을 하기는 어렵습니다.

Frame의 Navigate() 메소드와 각종 이벤트들을 사용해야 하기 때문인데, 이렇게 뷰모델에서 직접 접근을 하기 어려운 컨트롤이나 컴포넌트를 쉽게 제어하고 사용하기 위해서는 Behavior을 만들어 주어야 합니다.

 

FrameBehavior.cs

작동 방식은 FrameBehavior에 추가한 NavigationSource DP를 ViewModel의 프로퍼티와 바인딩을 한 후 ViewModel에서 프로퍼티를 변경하면, 그 내용을 전달 받은 FrameBehavior가 내용을 확인해서, Navigate()를 호출하던가, GoBack을 하고 됩니다.

또한, 네비게이션이 시작시, 종료시 ViewModel에 해당 내용을 알려줘서, 추가적인 작업을 지시할 수 있습니다.

using Microsoft.Xaml.Behaviors;
using System;
using System.Windows;
using System.Windows.Controls;
using WpfFramework.Interfaces;

namespace WpfFramework.Behaviors
{
    /// <summary>
    /// Frame 비헤이비어
    /// </summary>
    public class FrameBehavior : Behavior<Frame>
    {
        /// <summary>
        /// NavigationSource DP 변경 때문에 발생하는 프로퍼티 체인지 이벤트를 막기 위해 사용
        /// </summary>
        private bool _isWork;

        protected override void OnAttached()
        {
            //네비게이션 시작
            AssociatedObject.Navigating += AssociatedObject_Navigating;
            //네비게이션 종료
            AssociatedObject.Navigated += AssociatedObject_Navigated;
        }
        /// <summary>
        /// 네비게이션 종료 이벤트 핸들러
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AssociatedObject_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
        {
            _isWork = true;
            //네비게이션이 완료된 Uri를 NavigationSource에 입력
            NavigationSource = e.Uri.ToString();
            _isWork = false;
            //네비게이션이 완료된 상황을 뷰모델에 알려주기
            if (AssociatedObject.Content is Page pageContent
                && pageContent.DataContext is INavigationAware navigationAware)
            {
                navigationAware.OnNavigated(sender, e);
            }
        }
        /// <summary>
        /// 네비게이션 시작 이벤트 핸들러
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AssociatedObject_Navigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e)
        {
            //네비게이션 시작전 상황을 뷰모델에 알려주기
            if (AssociatedObject.Content is Page pageContent
                && pageContent.DataContext is INavigationAware navigationAware)
            {
                navigationAware?.OnNavigating(sender, e);
            }
        }

        protected override void OnDetaching()
        {
            AssociatedObject.Navigating -= AssociatedObject_Navigating;
            AssociatedObject.Navigated -= AssociatedObject_Navigated;
        }

        public string NavigationSource
        {
            get { return (string)GetValue(NavigationSourceProperty); }
            set { SetValue(NavigationSourceProperty, value); }
        }

        /// <summary>
        /// NavigationSource DP
        /// </summary>
        public static readonly DependencyProperty NavigationSourceProperty =
            DependencyProperty.Register(nameof(NavigationSource), typeof(string), typeof(FrameBehavior), new PropertyMetadata(null, NavigationSourceChanged));

        /// <summary>
        /// NavigationSource PropertyChanged
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void NavigationSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (FrameBehavior)d;
            if (behavior._isWork)
            {
                return;
            }
            behavior.Navigate();
        }
        /// <summary>
        /// 네비게이트
        /// </summary>
        private void Navigate()
        {
            switch (NavigationSource)
            {
                case "GoBack":
                    //GoBack으로 오면 뒤로가기
                    if (AssociatedObject.CanGoBack)
                    {
                        AssociatedObject.GoBack();
                    }
                    break;
                case null:
                case "":
                    //아무것도 안함
                    return;
                default:
                    //navigate
                    AssociatedObject.Navigate(new Uri(NavigationSource, UriKind.RelativeOrAbsolute));
                    break;
            }
        }
    }
}

2. INavigationAware.cs

네비게이션의 시작, 종료를 뷰모델에 전달해주기 위해서 간단한 인터페이스를 하나 추가했습니다.

namespace WpfFramework.Interfaces
{
    /// <summary>
    /// 네비게이션 시작, 종료되는 시점을 뷰모델에 알려주는 인터페이스
    /// </summary>
    public interface INavigationAware
    {
        void OnNavigating(object sender, object navigationEventArgs);
        void OnNavigated(object sender, object navigatedEventArgs);
    }
}

3. ViewModelBase.cs

INavigationAware를 ViewModelBase의 인터페이스로 추가하고, 2개의 메서드를 구현하면서, virtual 메소드로 변경했습니다. 이제 뷰모델에서 해당 메소드들을 사용하려면, override해서 사용하면 됩니다.

using Microsoft.Toolkit.Mvvm.ComponentModel;
using WpfFramework.Interfaces;

namespace WpfFramework.Bases
{
    /// <summary>
    /// 뷰모델 베이스
    /// </summary>
    public abstract class ViewModelBase : ObservableObject, INavigationAware
    {
        private string _title;
        /// <summary>
        /// 타이틀
        /// </summary>
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        private string _message;
        /// <summary>
        /// 메시지
        /// </summary>
        public string Message
        {
            get { return _message; }
            set { SetProperty(ref _message, value) ; }
        }
        /// <summary>
        /// 네비게이션 완료시
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="navigatedEventArgs"></param>
        public virtual void OnNavigated(object sender, object navigatedEventArgs)
        {
        }
        /// <summary>
        /// 네비게이션 시작시
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="navigationEventArgs"></param>
        public virtual void OnNavigating(object sender, object navigationEventArgs)
        {
        }
    }
}

4. MainWindow.xaml

xaml에서 behavior를 사용하기 위해서는 xmlns:behaviors="clr-namespace:WpfFramework.Behaviors" 네임스페이스를 추가해야합니다.

Frame 컨트롤에 위에서 만든 FrameBehavior을 추가해주고, NavigationSource DP에는 뷰모델에 있는 NavigationSource 프로퍼티를 TwoWay로 바인딩 해줍니다.

 

Master메뉴를 추가하고, 하위 메뉴로 Code Management, Customer Management를 추가했습니다.

Customer Management 메뉴를 클릭하면, 뷰모델의 NavigateCommand를 실행하면서, CommandParameter로는 Views/CustomerPage.xaml를 전달합니다.

<Window
    x:Class="WpfFramework.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:behaviors="clr-namespace:WpfFramework.Behaviors"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfFramework"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="{Binding Title}"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <DockPanel>
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_File">
                <MenuItem Header="_New" />
                <MenuItem Header="_Open" />
                <MenuItem Header="_Save" />
                <Separator />
                <MenuItem Header="_Exit" />
            </MenuItem>
            <MenuItem Header="_Master">
                <MenuItem Header="_Code Management" />
                <MenuItem
                    Command="{Binding NavigateCommand}"
                    CommandParameter="Views/CustomerPage.xaml"
                    Header="C_ustomer Management" />
            </MenuItem>
        </Menu>
        <Frame NavigationUIVisibility="Hidden">
            <b:Interaction.Behaviors>
                <behaviors:FrameBehavior NavigationSource="{Binding NavigationSource, Mode=TwoWay}" />
            </b:Interaction.Behaviors>
        </Frame>
    </DockPanel>
</Window>

5. MainViewModel.cs

NavigationSource 프로퍼티와 NavigateCommand를 추가합니다.

NavigationSource 프로퍼티를 변경하는 2가지 방법이 있습니다. 하나는 NavigateCommand를 실행하는 것이고, 다른 하나는 Toolkit.Mvvm에 존재하는 기능인 Message 기능을 활용하는 것입니다.

 

네비게이션 방법을 정리 하면 다음과 같습니다.

  • 메뉴에서 NavigateCommand와 페이지 주소를 입력 받음 -> NavigationSource 변경 -> FrameBehavior.NavigationSource DP의 값 변경 -> PropertyChanged가 발생되면, FrameBehavior 내부에 Navigate() 메소드가 실행되면서 페이지 이동 -> 페이지가 이동되기전에 뷰모델에 OnNavigating 메소드 실행 -> 페이지 이동 완료 후 OnNavigated 메소드 실행
  • 다른 화면에서 뒤로가기 버튼 클릭 -> Messenger를 이용해서 NavigationMessage 발생 -> MainViewModel에서 메시지 수신 -> NavigationSource 변경 -> 나머지는 위와 같음

MainWindow가 실행되면서, 최초로 보일 화면은 Views/HomePage.xaml 입니다.

using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.Toolkit.Mvvm.Messaging;
using System.Windows.Input;
using WpfFramework.Bases;
using WpfFramework.Models;

namespace WpfFramework.ViewModels
{
    /// <summary>
    /// 메인 뷰모델 클래스
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        private string _navigationSource;
        /// <summary>
        /// 네비게이션 소스
        /// </summary>
        public string NavigationSource
        {
            get { return _navigationSource; }
            set { SetProperty(ref _navigationSource, value); }
        }
        /// <summary>
        /// 네비게이트 커맨드
        /// </summary>
        public ICommand NavigateCommand { get; set; }

        /// <summary>
        /// 생성자
        /// </summary>
        public MainViewModel()
        {
            Title = "Main View";
            Init();
        }

        private void Init()
        {
            //시작 페이지 설정
            NavigationSource = "Views/HomePage.xaml";
            NavigateCommand = new RelayCommand<string>(OnNavigate);

            //네비게이션 메시지 수신 등록
            WeakReferenceMessenger.Default.Register<NavigationMessage>(this, OnNavigationMessage);
        }

        /// <summary>
        /// 네비게이션 메시지 수신 처리
        /// </summary>
        /// <param name="recipient"></param>
        /// <param name="message"></param>
        private void OnNavigationMessage(object recipient, NavigationMessage message)
        {
            NavigationSource = message.Value;
        }

        private void OnNavigate(string pageUri)
        {
            NavigationSource = pageUri;
        }
    }
}

6. NavigationMessage.cs

Messenger에서 사용할 수 있는 메시지는 간단하게 만들 수 있습니다.

using Microsoft.Toolkit.Mvvm.Messaging.Messages;

namespace WpfFramework.Models
{
    /// <summary>
    /// 네비게이션 메시지
    /// </summary>
    public class NavigationMessage : ValueChangedMessage<string>
    {
        public NavigationMessage(string value) : base(value)
        {
        }
    }
}

7. CustomerPage.xaml

Customer Management 메뉴를 선택하면 이동하는 페이지입니다. 

이 화면은 뒤로가기 버튼이 존재합니다.

<Page
    x:Class="WpfFramework.Views.CustomerPage"
    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:local="clr-namespace:WpfFramework.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="CustomerPage"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <Button
                Width="50"
                Command="{Binding BackCommand}"
                Content="Back" />
        </StackPanel>
        <StackPanel
            Grid.Row="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
            <TextBlock FontSize="20" Text="{Binding Title}" />
            <TextBlock Text="{Binding Message}" />
        </StackPanel>
    </Grid>
</Page>

8. CustomerViewModel.cs

CustomerViewModel입니다. 

BackCommand를 호출하면, Messenger를 통해서 new NavigationMessage("GoBack") 메시지를 전달합니다. 이 메시지의 수신 및 처리는 MainViewModel에서 합니다.

using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.Toolkit.Mvvm.Messaging;
using System.Windows.Input;
using WpfFramework.Bases;
using WpfFramework.Models;

namespace WpfFramework.ViewModels
{
    public class CustomerViewModel : ViewModelBase
    {
        public ICommand BackCommand { get; set; }

        public CustomerViewModel()
        {
            Init();
        }

        private void Init()
        {
            Title = "Customer";
            BackCommand = new RelayCommand(OnBack);
        }

        private void OnBack()
        {
            WeakReferenceMessenger.Default.Send(new NavigationMessage("GoBack"));
        }

        public override void OnNavigated(object sender, object navigatedEventArgs)
        {
            Message = "Navigated";
        }
    }
}

9. App.xaml.cs

새로 추가한 뷰모델들은 여기에 인젝션이 가능하도록 등록해 줘야 합니다.

        /// <summary>
        /// Configures the services for the application.
        /// </summary>
        private static IServiceProvider ConfigureServices()
        {
            var services = new ServiceCollection();

            //ViewModel 등록
            services.AddTransient(typeof(MainViewModel));
            services.AddTransient(typeof(HomeViewModel));
            services.AddTransient(typeof(CustomerViewModel));

            return services.BuildServiceProvider();
        }

HomePage.xaml, HomeViewModel.cs는 스크롤이 너무 많아져서 소스로 확인하시면 좋을 것 같습니다.

10. 실행 결과

Customer Management 메뉴를 선택해서 네비게이션

Back 버튼을 눌러서 Home 화면으로 이동

11. 소스

전체 소스는 아래 github에 올리고 있습니다. 다만, 브랜치를 머지하지 않기 때문에 필요한 소스는 각 브랜치를 다운로드 받아서 확인하셔야 합니다.

이번회차 브랜치는 part2/add-navigation-function 입니다.

kaki104/WpfFramework (github.com)

 

GitHub - kaki104/WpfFramework

Contribute to kaki104/WpfFramework development by creating an account on GitHub.

github.com

브랜치의 소스를 확인하는 방법

master를 클릭하시면 브랜치 목록이 나오고, 원하는 브랜치를 클릭하시면 소스를 확인하실 수 있습니다.

Visual Studio에서 확인하는 방법

master 브랜치의 소스를 다운로드 -> Git Changes -> Remotes -> 원하는 브랜치 선택하시면 소스가 다운로드되면서 내용을 확인하고 실행할 수 있습니다.

반응형
댓글