저번 포스팅에서는 프로그램이 시작될 때 모든 폴더 구조를 읽어서 트리형태로 미리 만들어두고 이를 TreeView에 Binding 했습니다. 최초 실행 시 시간이 오래 걸린다는 점과 필요 이상의 메모리를 사용한다는 문제가 있습니다. 이를 해결하려면 폴더의 정보가 필요한 시점에 필요한 폴더 정보만 읽어오면 됩니다.
✅ TreeViewItem Expanding Event
결국 폴더를 확장할 때 하위 폴더 목록을 읽어서 현재 폴더(TreeViewItem)의 자식 아이템으로 추가하면 시작할 때 모든 폴더의 정보를 읽을 필요가 없습니다. 그럼 TreeViewItem의 Expanding 이벤트가 발생했을 때 위 내용을 처리하면 됩니다. 안타깝게도 Expanding 이벤트를 MVVM에서 사용하려면 Behavior를 사용해야 합니다. 여기서 문제가 발생합니다.
MVVM 패턴을 사용하여 개발하다보면 언젠가는 마주치게 되는 내용일 것 같습니다.
Codebehind를 사용하면 간단하게 해결될 수 있는 문제가 MVVM 패턴에서는 제법 까다로운 경우가 많습니다.
이 경우는 Codebehind를 대체하기 위해 Behavior 또는 Attached Property 등을 사용할 수 있는데 xaml의 Style과 딱 맞아떨어지지 않기 때문에 발생한 문제입니다.
자세한 설명은 아래 블로그에 잘설명되어있으니 꼭 참고하시기 바랍니다.
위 블로그에서도 언급한 StackOverFlow의 글도 참고하시면 이해하는데 도움이 될 듯합니다.
정리하면 Behavior는 컨트롤마다 새로 생성되어 동작하는데 Style에 넣어서 컨트롤에 적용할 경우 Behavior가 생성되지 않기 때문에 오류가 발생한다는 내용입니다.
TreeView의 TreeViewItem도 각각의 컨트롤로 처리되기 때문에 Style을 정의하여 동적으로 Behavior를 달아줘야 하는데 StackOverFlow의 질문처럼
<Style>
<Setter Property="i:Interaction.Behaviors">
<Setter.Value>
<local:MyBehavior />
</Setter.Value>
</Setter>
</Style>
Style에 Behavior를 넣으면 오류가 발생합니다.
이를 해결하기 위해서 Behavior를 생성해주는 DependencyProperty를 사용하여 해결한 내용입니다.
생성한 Behavior를 삭제하는 부분은 없어서 추가해줘야 할 것 같습니다.
위 내용들을 진행 중인 프로젝트에 적용하겠습니다.
Behavior를 사용하기 위해 nuget에서 Microsoft.Xaml.Behaviors.Wpf를 설치합니다.
SupplementaryInteraction.cs
오늘의 메인 코드입니다.
namespace Expanders
{
public class Behaviors : List<Behavior>
{
}
public class Triggers : List<Microsoft.Xaml.Behaviors.TriggerBase>
{
}
public static class SupplementaryInteraction
{
public static Behaviors GetBehaviors(DependencyObject obj)
{
return (Behaviors)obj.GetValue(BehaviorsProperty);
}
public static void SetBehaviors(DependencyObject obj, Behaviors value)
{
obj.SetValue(BehaviorsProperty, value);
}
public static readonly DependencyProperty BehaviorsProperty =
DependencyProperty.RegisterAttached(
"Behaviors",
typeof(Behaviors),
typeof(SupplementaryInteraction),
new UIPropertyMetadata(null, OnPropertyBehaviorsChanged));
private static void OnPropertyBehaviorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behaviors = Interaction.GetBehaviors(d);
foreach (var behavior in e.NewValue as Behaviors) behaviors.Add(behavior);
}
public static Triggers GetTriggers(DependencyObject obj)
{
return (Triggers)obj.GetValue(TriggersProperty);
}
public static void SetTriggers(DependencyObject obj, Triggers value)
{
obj.SetValue(TriggersProperty, value);
}
public static readonly DependencyProperty TriggersProperty =
DependencyProperty.RegisterAttached(
"Triggers",
typeof(Triggers),
typeof(SupplementaryInteraction),
new UIPropertyMetadata(null, OnPropertyTriggersChanged));
private static void OnPropertyTriggersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var triggers = Interaction.GetTriggers(d);
foreach (var trigger in e.NewValue as Triggers) triggers.Add(trigger);
}
}
}
View - MainView.xaml
TreeView 부분은 아래와 같이 수정합니다.
<UserControl x:Class="Explorer.MainView"
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:Explorer"
xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
xmlns:expanders="clr-namespace:Expanders"
xmlns:vm="clr-namespace:ExplorerViewModel;assembly=ExplorerViewModel"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<TreeView DataContext="{Binding DirectoryTreeVM}"
Grid.Row="1" Grid.RowSpan="1" Grid.Column="0" Grid.ColumnSpan="1" ItemsSource="{Binding DirectoryTreeCollection.Root.Children}">
<TreeView.Resources>
<expanders:Triggers x:Key="expandingTrigger" x:Shared="False">
<behaviors:EventTrigger EventName="Expanded">
<behaviors:InvokeCommandAction Command="{Binding DataContext.ExpandingTreeViewItemCommand, RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type TreeView}}}" CommandParameter="{Binding}"/>
</behaviors:EventTrigger>
</expanders:Triggers>
<HierarchicalDataTemplate DataType="{x:Type vm:DirectoryTreeNode}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Data.Name}"/>
</HierarchicalDataTemplate>
<Style TargetType="TreeViewItem">
<Setter Property="expanders:SupplementaryInteraction.Triggers" Value="{StaticResource expandingTrigger}"/>
</Style>
</TreeView.Resources>
</TreeView>
Expanding 이벤트가 발생하면 ViewModel의 ExpandingTreeViewItemCommand로 Notify가 전달됩니다.
ViewModel - DirectoryTreeViewModel.cs
ViewModel에는 ICommand를 추가합니다.
public class DirectoryTreeViewModel : BaseViewModel
{
private ICommand _expandingTreeViewItemCommand;
public ICommand ExpandingTreeViewItemCommand
{
get
{
if (_expandingTreeViewItemCommand == null)
_expandingTreeViewItemCommand = new RelayCommand<object>(OnExpandingTreeViewItem);
return _expandingTreeViewItemCommand;
}
}
private void OnExpandingTreeViewItem(object obj)
{
// Expanding 이벤트 최종 호출 위치
}
}
실행
확장을 하면 OnExpandingTreeViewItem 가 호출되며 argument로 넘어온 obj에는 TreeViewItem의 DataContext가 들어있습니다.
조금 어렵기도 하지만 WPF의 MVVM에서 중요한 내용입니다.
알아두시면 좋을 듯합니다.
다음 포스팅에서는 동적으로 하위 폴더를 추가하도록 해보겠습니다.
✅ TreeViewItem Expanding Event - 끝
관련 포스팅