[C# WPF] Button을 눌러 Frame에 Page 띄우기-1

우롱밀크티당도70·2023년 9월 11일
0

WPF

목록 보기
6/22
post-custom-banner

1. 배경

로그인 화면이 있고 로그인 한 사용자 타입에 따라 보여지는 메뉴 항목과 내용이 달라지게 하고싶다.
코드 비하인드에서 MainWindow 내에 배치한 Button으로 마찬가지로 MainWindow 내에 배치되어 있는 Frame에 화면을 전환하는 방식은 구글링으로 제법 찾아봤는데 MVVM 패턴으로 구현된건 찾아보기가 힘들었다.
사실 안나오는데엔 이유가 있다고도 보는데 아무튼간에...


2. 개발환경

  • VisualStudio 2022

3. 내용

3-1. MVVM 패턴의 폴더 구조 구성

3-2. View 파일 생성

  • MenuBar1과 MenuBar2는 Page이다.
  • LoginWindow -> MainWindow가 열릴 예정이기 때문에 App.xaml의 StartupUri의 경로를 Views/LoginWindow.xaml로 수정했다.
	<Application x:Class="PracticeProject.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:PracticeProject"
             StartupUri="Views/LoginWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

3-3. MainWindow Grid 설정

<Window x:Class="PracticeProject.MainWindow"
        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:PracticeProject"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100*"/>
            <ColumnDefinition Width="700*"/>
        </Grid.ColumnDefinitions>

        <!--메뉴바-->
        <Grid Grid.Column="0">
            <Frame x:Name="MenuBar" NavigationUIVisibility="Hidden" />
        </Grid>
        
        <!--콘텐츠-->
        <Grid Grid.Column="1">
            <Frame x:Name="Contents" NavigationUIVisibility="Hidden" />
        </Grid>

    </Grid>
</Window>
  • Grid로 왼쪽은 메뉴가 보이고 오른쪽엔 메뉴에 따른 내용이 보이도록 할 것이다.
  • Frame 컨트롤에 x:Name으로 이름을 지어준다.

3-4. LoginWindow.xaml 컨트롤 배치

<Window x:Class="PracticeProject.Views.LoginWindow"
        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:PracticeProject.Views"
        mc:Ignorable="d"
        Title="LoginWindow" Height="450" Width="800">
    
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
            <Label Content="아이디"/>
            <TextBox Width="200"/>

            <Label Content="비밀번호"/>
            <PasswordBox Width="200"/>

            <Button Content="로그인" Margin="0,10,0,0"/>
        </StackPanel>
        
    </Grid>
</Window>
  • Binding은 Command를 작성한 뒤에 한다.

3-5. MenuBar1.xaml MenuBar2.xaml(Page) 컨트롤 배치

  • MenuBar1.xaml
<Page x:Class="PracticeProject.Views.MenuBar1"
      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:PracticeProject.Views"
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="100"
      Title="MenuBar1">

    <Grid>
        <StackPanel>
            <Button Content="Menu1" Height="30" />
            <Button Content="Menu2" Height="30" />
            <Button Content="Menu3" Height="30" />
        </StackPanel>
        
    </Grid>
</Page>
  • MenuBar2.xaml
<Page x:Class="PracticeProject.Views.MenuBar2"
      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:PracticeProject.Views"
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="100"
      Title="MenuBar2">

    <Grid>
        <StackPanel>
            <Button Content="Menu4" Height="30" />
            <Button Content="Menu5" Height="30" />
            <Button Content="Menu6" Height="30" />
        </StackPanel>
    </Grid>
</Page>
  • LoginWindow에서 user로 로그인하면 MenuBar1.xaml을 보여주고 Manager로 로그인하면 MenuBar2.xaml을 보여주게 될 것이다.

3-6. Commands/RelayCommand.cs 작성

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

namespace PackagingSystem.Commands
{
    public class RelayCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        public RelayCommand(Action execute, Func<bool> canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

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

        public bool CanExecute(object parameter) => _canExecute == null || _canExecute();

        public void Execute(object parameter) => _execute();
    }
}

3-7. ViewModels/BaseViewModel.cs 작성

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

namespace PackagingSystem.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        protected bool SetProperty<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingField, value)) return false;
            backingField = value;
            OnPropertyChanged(propertyName);
            return true;
        }
    }
}
  • 다른 ViewModel 클래스 파일들이 BaseViewModel을 상속받을 것이다.

3-8. ViewModels/LoginWindowModel.cs 작성

  • LoginWindow에서 id와 password를 입력하면 MainWindow를 띄우는 데에 필요한 Binding과 Command 작성이 필요하다.
  1. LoginWindowModel이 BaseViewModel을 상속 받는다.
  2. id와 password 변수를 선언한다. 해당 클래스 외에서 접근할 수 없도록 private으로...(캡슐화를 위해)

  1. _id와 _password의 getter, setter를 작성한다.

  1. Command와 Event를 작성한다.

    C#을 제대로 기초부터 배워본 적이 없어서...설명을 못하겠다!

  1. segUser는 임의로...id와 password값에 따라 return하는 값이 달라지도록 한다.
    1이면 user, 2면 manager라는 뜻.

  1. MainWindow의 Instance를 가져온다. 새 창으로 MainWindow를 띄우는데 segNum의 return 값에 따라 MainWindow.xaml에서 작성한 Frame(x:name이 MenuBar였던 것)의 Source값을 다르게 한다.

  • LoginWindowModel.cs 코드 전체
using PackagingSystem.Commands;
using PackagingSystem.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;

namespace PracticeProject.ViewModels
{
    class LoginWindowModel : BaseViewModel
    {
        private string _id;
        private string _password;

        public string Id
        {
            get { return  _id; }
            set 
            {
                _id = value;
                OnPropertyChanged(nameof(Id));
            }
        }

        public string Password
        {
            get { return _password; }
            set
            {
                _password = value;
                OnPropertyChanged(nameof(Password));
            }
        }

        public ICommand LoginCommand { get; set; }

        public event Action RequestClose;

        public LoginWindowModel() 
        {
            LoginCommand = new RelayCommand(Login);
        }

        private void Login()
        {
            int iSegNum = segUser(Id, Password);
            if(iSegNum > 0)
            {
                MainWindow mainWindow = MainWindow.GetInstance();
                mainWindow.Show();

                if(iSegNum == 1)
                {
                    mainWindow.MenuBar.Source = new Uri("MenuBar1.xaml", UriKind.Relative);
                } 
                else if(iSegNum == 2)
                {
                    mainWindow.MenuBar.Source = new Uri("MenuBar2.xaml", UriKind.Relative);
                }
                RequestClose?.Invoke(); // 이벤트 발생
            }
            else
            {
                MessageBox.Show("아이디, 패스워드가 일치하지 않습니다.");
            }
        }

        private int segUser(string id, string password)
        {
            int segNum = 0;

            if(id == "user" && password == "user")
            {
                segNum = 1;
            }
            else if(id == "manager" && password == "manager")
            {
                segNum = 2;
            }
            return segNum;

        }
    }
}

3-9. MainWindow의 GetInstance()

  • MainWindow.xaml.cs(코드 비하인드)에서 싱글톤 패턴 GetInstance() 메소드를 만들어주어야 한다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace PracticeProject
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private static MainWindow _Instance = null;

        public static MainWindow GetInstance()
        {
        	if (_Instance == null)
            {
                _Instance = new MainWindow();
            }
            return _Instance 
        }

        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

3-10. Id와 Password Binding, Button Command

  • 다시 LoginWindow.xaml에서 id와 password를 입력하는 TextBox와 PasswordBox, 로그인 Button에 Binding, Command를 작성한다.
<Window x:Class="PracticeProject.Views.LoginWindow"
        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:PracticeProject.Views"
        xmlns:vm="clr-namespace:PracticeProject.ViewModels"
        mc:Ignorable="d"
        Title="LoginWindow" Height="450" Width="800">

    <Window.DataContext>
        <vm:LoginWindowModel />
    </Window.DataContext>
    
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
            <Label Content="아이디"/>
            <TextBox Width="200" Text="{Binding Id}"/>

            <Label Content="비밀번호"/>
            <PasswordBox Width="200" PasswordChanged="OnPasswordChanged"/>

            <Button Content="로그인" Command="{Binding LoginCommand}" Margin="0,10,0,0"/>
        </StackPanel>
        
    </Grid>
</Window>
  • TextBox는 Text 속성에 Binding이 가능하지만 PasswordBox는 보안상의 이유로 Binding이 불가능하다. 그래서 PasswordChanged 속성에 이벤트를 작성한다.

  • LoginWindow.xaml.cs(코드 비하인드)

using PracticeProject.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace PracticeProject.Views
{
    /// <summary>
    /// LoginWindow.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class LoginWindow : Window
    {

        public LoginWindow()
        {
            InitializeComponent();

            var viewModel = new LoginWindowModel();
            DataContext = viewModel;
            
        }

        private void OnPasswordChanged(object sender, RoutedEventArgs e)
        {
            if (DataContext is LoginWindowModel viewModel)
            {
                viewModel.Password = ((PasswordBox)sender).Password;
            }
        }
    }
}

4. 결과

  • id : user, password : user로 로그인했을 때

  • id : manager, password : manager로 로그인했을 때

5. 마무리

로그인 유형에 따라 각각 다른 메뉴 바(page)가 나오게 했다.
사실 막 찾아볼 때 ViewModel에서 View 혹은 컨트롤의 프로퍼티를 직접 바인딩해서 조작하는 방식은 사용하면 안된다고 했는데(내용 : https://kaki104.tistory.com/531#google_vignette) 이것 말고는 어떻게 해야할지 잘 모르겠다.
그리고 코드 비하인드를 될 수 있으면 작성하지 않는 것에 너무 집착하다보니 사용할 생각을 아예 못했다...
각 메뉴를 눌렀을때 오른쪽 Frame(x:Name을 Contents로 지정한 것)이 전환되는 것을 다음 글에 적기로 한다...

profile
안뇽하세용
post-custom-banner

0개의 댓글