본문 바로가기
Study/WPF

[WPF][C#][Study] WPF 시작 6일차

by 스테디코디스트 2023. 9. 20.
반응형

1. 데이터 바인딩

- 두 개의 데이터/ 정보에 대한 소스를 결합하고 데이터 동기화를 유지하는 기술

- source와 destination UI 구성간에 바인딩을 주로 함

- {Binding} 이렇게 중괄호 사이에 넣어서 사용

[예제]

<Window x:Class="WpfTutorialSamples.DataBinding.HelloBoundWorldSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="HelloBoundWorldSample" Height="110" Width="280">
    <StackPanel Margin="10">
		<TextBox Name="txtValue" />
		<WrapPanel Margin="0,10">
			<TextBlock Text="Value: " FontWeight="Bold" />
			<TextBlock Text="{Binding Path=Text, ElementName=txtValue}" />
		</WrapPanel>
	</StackPanel>
</Window>

[결과]

- TextBox의 Name을 txtValue로 설정

- TextBlock의 Text에 바인딩을 시킴 => Binding Path를 Text로 연결 + ElementName을  txtValue로 설정

=> TextBlock의 Text가 현재 바인딩 하고 있는 TextBox에 사용자가 적은 Text가 그대로 연결되어 나옴

 

1-1. DataContext

- 바인딩의 기본 소스

- 기본 설정값은 따로 없음(모두 null 값을 가짐)

[예제]

1) XAML

<Window x:Class="WpfTutorialSamples.DataBinding.DataContextSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DataContextSample" Height="130" Width="280">
	<StackPanel Margin="15">
		<WrapPanel>
			<TextBlock Text="Window title:  " />
			<TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Width="150" />
		</WrapPanel>
        
		<WrapPanel Margin="0,10,0,0">
			<TextBlock Text="Window dimensions: " />
			<TextBox Text="{Binding Width}" Width="50" />
			<TextBlock Text=" x " />
			<TextBox Text="{Binding Height}" Width="50" />
		</WrapPanel>
	</StackPanel>
</Window>

2) Code-Behind

using System;
using System.Windows;

namespace WpfTutorialSamples.DataBinding
{
	public partial class DataContextSample : Window
	{
		public DataContextSample()
		{
			InitializeComponent();
            
            // DataContext의 내용을 자기자신의 내용으로 함
			this.DataContext = this;
		}
	}
}

3) 결과

- 첫번째 WrapPanel의 TextBox의 Text는 Title에 연결됨

- 두번째 WrapPanel의 첫번째 TextBox의 Text는 Width에 연결되고 두번째 TextBox의 Text는 Height에 연결됨

- Code-Behind에서 Context의 내용은 자기자신으로 정의하였으므로, 각 Title과 Width, Height는 자기자신의 것과 연결되어 화면에 자기자신의 Title, Width, Height가 보여지게 된다.

 

1-2. Code-Behind로 바인딩

- Binding 인스턴스를 만들어 작동 -> 생성자에서 직접 원하는 경로를 지정

(ex) Binding b = new Binding("Text");

- 소스를 지정해야 함(바인딩 할 대상)

- 실제 연결 -> SetBinding 이용 -> 바인딩하는 자신의 속성과 바인딩 인스턴스를 매개 변수로 받음

[예제]

1) xaml

<Window x:Class="WpfTutorialSamples.DataBinding.CodeBehindBindingsSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="CodeBehindBindingsSample" Height="110" Width="280">
    <StackPanel Margin="10">
    	<TextBox Name="txtValue" />
    
    <WrapPanel Margin="0,10">
        <TextBlock Text="Value: " FontWeight="Bold" />
        <TextBlock Name="lblValue" />
    </WrapPanel>
    </StackPanel>
</Window>

2) code-behind

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfTutorialSamples.DataBinding
{
    public partial class CodeBehindBindingsSample : Window
    {
        public CodeBehindBindingsSample()
        {
            InitializeComponent();

            Binding binding = new Binding("Text"); // 바인딩 인스턴스 생성
            binding.Source = txtValue; // 소스 연결
            lblValue.SetBinding(TextBlock.TextProperty, binding); // 실제 바인딩
        }
    }
}

3) 결과

- Text 속성의 바인딩 인스턴스 생성

- TextBox txtValue를 소스로 연결

- TextBlock lblValue에 바인딩

 

1-3. UpdateSourceTrigger 속성

- 변경된 내용이 반영되는 것을 조절함

- Defult가 기본값, 다른 옵션들로는 PropertyChanged, LostFocus, Explicit 이 있음

[예제]

1) xaml

<Window x:Class="WpfTutorialSamples.DataBinding.DataContextSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DataContextSample" Height="130" Width="310">
	<StackPanel Margin="15">
		<WrapPanel>
			<TextBlock Text="Window title:  " />
			<TextBox Name="txtWindowTitle" Text="{Binding Title, UpdateSourceTrigger=Explicit}" Width="150" />
			<Button Name="btnUpdateSource" Click="btnUpdateSource_Click" Margin="5,0" Padding="5,0">*</Button>
		</WrapPanel>
        
		<WrapPanel Margin="0,10,0,0">
			<TextBlock Text="Window dimensions: " />
			<TextBox Text="{Binding Width, UpdateSourceTrigger=LostFocus}" Width="50" />
			<TextBlock Text=" x " />
			<TextBox Text="{Binding Height, UpdateSourceTrigger=PropertyChanged}" Width="50" />
		</WrapPanel>
	</StackPanel>
</Window>

2) code-behind

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfTutorialSamples.DataBinding
{
	public partial class DataContextSample : Window
	{
		public DataContextSample()
		{
			InitializeComponent();
			this.DataContext = this;
		}

		private void btnUpdateSource_Click(object sender, RoutedEventArgs e)
		{
			// 버튼을 누르면 값이 적용되도록 함
			BindingExpression binding = txtWindowTitle.GetBindingExpression(TextBox.TextProperty);
			binding.UpdateSource();
		}
	}
}

3) 결과

- 첫번째 Title은 Explicit으로 설정 -> 직접 업데이트를 수행하지 않으면 내용이 변경되지 않음(버튼을 누르면 변경됨)

- 두번째 Width는 LostFocus로 설정 -> 포커스가 바뀔 때마다 값이 없데이트 됨

- 세번째 Height는 PropertyChanged로 설정 -> 바인딩 속성이 변경될 때마다 즉시 변경됨

 

1-4. ObservableCollection, INotifyPropertyChanged 

[ObservableCollection<T>]

- List<T> 와 역할은 거의 유사

- INotify~ 이렇게 생긴 인터페이스가 상속된 개체들에 변화가 생기면 UI에 알려주어 자동으로 업데이트 시켜줌

 

[예제]

1) xaml

<Window x:Class="WpfTutorialSamples.DataBinding.ChangeNotificationSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ChangeNotificationSample" Height="135" Width="300">
	<DockPanel Margin="10">
		<StackPanel DockPanel.Dock="Right" Margin="10,0,0,0">
			<Button Name="btnAddUser" Click="btnAddUser_Click">Add user</Button>
			<Button Name="btnChangeUser" Click="btnChangeUser_Click" Margin="0,5">Change user</Button>
			<Button Name="btnDeleteUser" Click="btnDeleteUser_Click">Delete user</Button>
		</StackPanel>
		<ListBox Name="lbUsers" DisplayMemberPath="Name"></ListBox>
	</DockPanel>
</Window>

2) code-behind

using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace WpfTutorialSamples.DataBinding
{
	public partial class ChangeNotificationSample : Window
	{
		//private List<User> users = new List<User>() // -> UI 갱신이 안됨 -> ObservableCollection 사용
		private ObservableCollection<User> users = new ObservableCollection<User>();

		public ChangeNotificationSample()
		{
			InitializeComponent();

			users.Add(new User() { Name = "John Doe" });
			users.Add(new User() { Name = "Jane Doe" });

			lbUsers.ItemsSource = users;
		}

		private void btnAddUser_Click(object sender, RoutedEventArgs e)
		{
			users.Add(new User() { Name = "New user" });
		}

		private void btnChangeUser_Click(object sender, RoutedEventArgs e)
		{
        	// 선택된 아이템이 있는 경우에만 동작 -> 선택 아이템의 이름 변경
			if(lbUsers.SelectedItem != null)
				(lbUsers.SelectedItem as User).Name = "Random Name";
		}

		private void btnDeleteUser_Click(object sender, RoutedEventArgs e)
		{
        	// 선택된 아이템이 있는 경우에만 동작 -> 선택 아이템 삭제
			if(lbUsers.SelectedItem != null)
				users.Remove(lbUsers.SelectedItem as User);
		}
	}

	public class User : INotifyPropertyChanged
	{
		private string name;
		public string Name {
			get { return this.name; }
			set
			{
				if(this.name != value)
				{
					this.name = value;
					this.NotifyPropertyChanged("Name");
				}
			}
		}

		public event PropertyChangedEventHandler PropertyChanged;

		public void NotifyPropertyChanged(string propName)
		{
			if(this.PropertyChanged != null)
				this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
		}
	}
}

3) 결과

 

1-5. 값 변환

- 호환되지 않는 데이터를 바인딩 시켜야 할 경우 사용

- 예를 들어, 파일의 크기에 따라 바이트, 킬로바이트, 메가이트 등 단위로 나워 표시 할 경우 또는 체크박스를 값으로 확인하려는데 값이 Boolean 값이 아니라 "yes" "no" 처럼 문자열인 경우 등이 있다.

[예제]

1) xaml

<Window x:Class="WpfTutorialSamples.DataBinding.ConverterSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:local="clr-namespace:WpfTutorialSamples.DataBinding"
        Title="ConverterSample" Height="140" Width="250">
	<Window.Resources>
		<local:YesNoToBooleanConverter x:Key="YesNoToBooleanConverter" />
	</Window.Resources>
    
	<StackPanel Margin="10">
		<TextBox Name="txtValue" />
		<WrapPanel Margin="0,10">
			<TextBlock Text="Current value is: " />
			<TextBlock Text="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource YesNoToBooleanConverter}}"></TextBlock>
		</WrapPanel>
		<CheckBox IsChecked="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource YesNoToBooleanConverter}}" Content="Yes" />
	</StackPanel>
</Window>

2) code-behind

using System;
using System.Windows;
using System.Windows.Data;

namespace WpfTutorialSamples.DataBinding
{
	public partial class ConverterSample : Window
	{
		public ConverterSample()
		{
			InitializeComponent();
		}
	}

	public class YesNoToBooleanConverter : IValueConverter
	{
		public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
		{
			switch(value.ToString().ToLower())
			{
				case "yes":
					return true;
				case "no":
					return false;
			}
			return false;
		}

		public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
		{
			if(value is bool)
			{
				if((bool)value == true)
					return "yes";
				else
					return "no";
			}
			return "no";
		}
	}
}

3) 결과

- 문자열을 이용해 체크박스의 값을 변경시키위해 값 변환 IValueConverter를 사용하였다.

 

1-6. StringFormat property

- 특정 값이 표시되는 방식만 변경하고 다른 유형으로는 변환할 필요가 없는 경우 사용

- 유연성이 다소 떨어지지만 사용이 간편함

[예제]

<Window x:Class="WpfTutorialSamples.DataBinding.StringFormatSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        <!-- 추가해야함-->
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        Title="StringFormatSample" Height="150" Width="250"
		Name="wnd">
	<StackPanel Margin="10">
		<TextBlock Text="{Binding ElementName=wnd, Path=ActualWidth, StringFormat=Window width: {0:#,#.0}}" />
		<TextBlock Text="{Binding ElementName=wnd, Path=ActualHeight, StringFormat=Window height: {0:C}}" />
		<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Date: {0:dddd, MMMM dd}}" />
		<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Time: {0:HH:mm}}" />
	</StackPanel>
</Window>

[결과]

- 첫번째, 두번째는 StringFormat이 Window의 너비와 높이를 바인딩하였고, 형식 문자열을 사용해 출력하였다.

- 세번째, 네번째는 현재 날짜를 가져와 각각 날짜와 시간에 바인딩하여 각각의 형식대로 출력하였다.

 

2. 커맨드

- 편의성을 위해 쓰는 단축키를 사용할 수 있다.

[예제 1]

1) xaml

<Window x:Class="WpfTutorialSamples.Commands.UsingCommandsSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="UsingCommandsSample" Height="100" Width="200">
    <Window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.New" Executed="NewCommand_Executed" CanExecute="NewCommand_CanExecute" />
    </Window.CommandBindings>

    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button Command="ApplicationCommands.New">New</Button>
    </StackPanel>
</Window>

2) code-behind

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;

namespace WpfTutorialSamples.Commands
{
	public partial class UsingCommandsSample : Window
	{
		public UsingCommandsSample()
		{
			InitializeComponent();
		}

		private void NewCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
		{
			// 아무 동작도 하지 않을 때 특정 커맨드가 사용 가능한지 여부
			e.CanExecute = true;
		}

		private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
		{
			//커맨드가 발동했을 때 동작
			MessageBox.Show("The New command was invoked");
		}
	}
}

3) 결과

- New 버튼을 눌렀을 때 메세지 창이 뜸

- ApplicationCommands.New이므로 커맨드 버튼인 Ctrl+N을 눌렀을 때도 같은 메세지 창이 뜸

[예제 2]

1) xaml

<Window x:Class="WpfTutorialSamples.Commands.CommandCanExecuteSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CommandCanExecuteSample" Height="200" Width="250">
        
    <Window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Cut" CanExecute="CutCommand_CanExecute" Executed="CutCommand_Executed" />
        <CommandBinding Command="ApplicationCommands.Paste" CanExecute="PasteCommand_CanExecute" Executed="PasteCommand_Executed" />
    </Window.CommandBindings>
    
    <DockPanel>
        <WrapPanel DockPanel.Dock="Top" Margin="3">
            <Button Command="ApplicationCommands.Cut" Width="60">_Cut</Button>
            <Button Command="ApplicationCommands.Paste" Width="60" Margin="3,0">_Paste</Button>
        </WrapPanel>
        
        <TextBox AcceptsReturn="True" Name="txtEditor" />
    </DockPanel>
</Window>

2) code-behind

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;

namespace WpfTutorialSamples.Commands
{
	public partial class CommandCanExecuteSample : Window
	{
		public CommandCanExecuteSample()
		{
			InitializeComponent();
		}

		private void CutCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
		{
			// 단어가 있고, 해당 단어가 선택된 경우에만 동작
			e.CanExecute = (txtEditor != null) && (txtEditor.SelectionLength > 0);
		}

		private void CutCommand_Executed(object sender, ExecutedRoutedEventArgs e)
		{
			txtEditor.Cut();
		}

		private void PasteCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
		{
			e.CanExecute = Clipboard.ContainsText();
		}

		private void PasteCommand_Executed(object sender, ExecutedRoutedEventArgs e)
		{
			txtEditor.Paste();
		}
	}
}

3) 결과

- 단어가 있고, 해당 단어를 아래 그림처럼 드래그해서 선택했을 경우에만 Cut버튼이나 커맨드 키인 Ctrl+X로 동작한다.

- Paste는 Cut한 단어가 있을 경우에만 동작한다.

[예제 3]

<Window x:Class="WpfTutorialSamples.Commands.CommandsWithCommandTargetSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CommandsWithCommandTargetSample" Height="200" Width="250">
    <DockPanel>
        <WrapPanel DockPanel.Dock="Top" Margin="3">
            <Button Command="ApplicationCommands.Cut" CommandTarget="{Binding ElementName=txtEditor}" Width="60">_Cut</Button>
            <Button Command="ApplicationCommands.Paste" CommandTarget="{Binding ElementName=txtEditor}" Width="60" Margin="3,0">_Paste</Button>
        </WrapPanel>
        <TextBox AcceptsReturn="True" Name="txtEditor" />
    </DockPanel>
</Window>

[결과]

- 바인딩을 이용해 Code-behind 작성 없이 예제2와 같은 기능을 구현했다.

 

[예제 4] 커스텀 커맨드

1) xaml

<Window x:Class="WpfTutorialSamples.Commands.CustomCommandSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:self="clr-namespace:WpfTutorialSamples.Commands"
        Title="CustomCommandSample" Height="150" Width="200">
        
    <Window.CommandBindings>
        <CommandBinding Command="self:CustomCommands.Exit" CanExecute="ExitCommand_CanExecute" Executed="ExitCommand_Executed" />
    </Window.CommandBindings>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <Menu>
            <MenuItem Header="File">
                <MenuItem Command="self:CustomCommands.Exit" />
            </MenuItem>
        </Menu>
        
        <StackPanel Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Button Command="self:CustomCommands.Exit">Exit</Button>
        </StackPanel>
    </Grid>
</Window>

2) code-behind

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;

namespace WpfTutorialSamples.Commands
{
	public partial class CustomCommandSample : Window
	{
		public CustomCommandSample()
		{
			InitializeComponent();
		}

		private void ExitCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
		{
			e.CanExecute = true;
		}

		private void ExitCommand_Executed(object sender, ExecutedRoutedEventArgs e)
		{
			Application.Current.Shutdown();
		}
	}

	public static class CustomCommands
	{
		public static readonly RoutedUICommand Exit = new RoutedUICommand
			(
				"Exit", "Exit", typeof(CustomCommands),
				new InputGestureCollection()
				{
                	// alt+F4 라는 새로운 커맨드 등록
					new KeyGesture(Key.F4, ModifierKeys.Alt)
				}
			);

		//Define more commands here, just like the one above
	}
}

3) 결과

- 직접 설정한 커맨드를 이용할 수 있다.