ViewModel에서 User Control 초기화 관련 문의드립니다.

안녕하세요. WPF MVVM 패턴으로 열심히 연습하면서 일전에 남겨주신 댓글 덕분에 많은 도움을 얻고 있습니다. 연습을 하다보니, 한 가지 이슈가 발생해서 이 부분을 어떻게 처리하실지 의견을 문의드려요.

(코드를 직접 공유하기에는 코드 도배가 될 것 같아서 고민거리를 최대한 자세히 언급드리겠습니다.) 현재 연습 중인 토이 프로젝트의 환경은 아래와 같습니다.

  • 하나의 Window(Frame)과 하나의 Page가 있습니다.
  • 해당 Page에 있는 Panel에 DataContext를 사용해서 ViewModel과 연결시켰습니다.
  • Panel 안에는 'DataContext’의 바인딩 받아서 처리하는 UserControl들이 있습니다.

원하는 방식은 Page 안에 별도의 Button(마치 모든 작업이 완료되서 Home 버튼 같은)을 이용해서 마치 윈도우 창을 닫았다가 재실행한 것처럼 전체 프로젝트를 초기화해주고 싶습니다.

하지만 Button으로 Trigger되는 ViewModel 내 함수에서 UserControl에서 참조하는 프로퍼티들을 초기화해도, 여전히 화면에서는 UserControls에선 기존 값을 그대로 갖고 있는 것을 확인하였습니다.

관련해서 구글링을 해보니, 관련 자료를 발견하긴 하였는데, 실제로 이렇게 적용하시나요? 아니면 다른 방법이 있는지 궁금합니다.

읽어주셔서 감사합니다.

2개의 좋아요

깃허브로 실행되는 샘플을 공유 주시면 도움을 드릴 수 있을 것 같습니다.

2개의 좋아요

@dimohy 관심 갖아주셔서 감사드립니다. 제가 사내망으로 간간이 시간날 때마다 공부겸 진행하는거라 오픈 깃을 통해 공유드리지 못할 것 같아요. 대신 관련 코드를 이곳에 공유드리겠습니다. 우선, 원인은 찾았는데 관련해서 방법을 아직 찾지 못해 고민 중입니다.

앞서 말해드린대로 해당 페이지의 TabControl 안에 있는 Combobox를 선택하고 Search버튼을 누르면, 결과 정보가 FileInitialListup UserControl로 호출되는 방식입니다. 그리고 Reset버튼을 누르면 해당 결과 정보가 초기화될 예정입니다.

<Page x:Class="WPF_ParsingXML.View.FilePage"
      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:WPF_ParsingXML.View"
      xmlns:vm="clr-namespace:WPF_ParsingXML.ViewModels"
      xmlns:uc="clr-namespace:WPF_ParsingXML.View.FileSubPages.UserControls"
      mc:Ignorable="d"
      d:DesignHeight="550"
      d:DesignWidth="800"
      Title="FilePage">
    <Page.Resources>
        <vm:FileVM x:Key="vm" />
        <Style x:Key="CheckBoxOneClick" />
    </Page.Resources>
    <DockPanel DataContext="{StaticResource vm}">
        <StackPanel Orientation="Vertical"
                    Width="700">
            <TabControl x:Name="FileTabControl"
                        Visibility="Visible"
                        SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}">            
                <TabItem Header="Search"
                         Name="SearchTab">
                    <StackPanel>
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Left"
                                    Margin="10">
                            <ComboBox x:Name="ComboModel"
                                      Width="150"
                                      Height="20"
                                      ItemsSource="{Binding filterings}"
                                      SelectedItem="{Binding SelectedFiltering}">
                                <ComboBox.ItemTemplate>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock   Text="{Binding ModelName}" />
                                        </StackPanel>
                                    </DataTemplate>
                                </ComboBox.ItemTemplate>
                            </ComboBox>
                            <Button Content="Search"
                                    Margin="10"
                                    FontSize="15"
                                    x:Name="Btn_Search"
                                    Command="{Binding fileSearchCommand}"
                                    CommandParameter="{Binding SelectedFiltering}"
                                    Cursor="Hand" />
                              <Button Content="Reset"
                                    FontSize="15"
                                    Cursor="Hand"
                                    x:Name="Btn_Reset"
                                    Command="{Binding resetCommand}"
                                    Width="80" />
                        </StackPanel>
                        <ScrollViewer Name="SearchScrollViewer"
                                      Margin="10">
                            <uc:FileInitialListup Visibility="Visible" />
                        </ScrollViewer>                       
                    </StackPanel>
                </TabItem>            
            </TabControl>
        </StackPanel>
    </DockPanel>
</Page>

FileInitialListup UserControl에서는 체크박스를 통해서 전체 혹은 일부 목록만을 선택을 할 수 있도록 하기 위해서 첫번째 Column에는 체크박스 관련 로직이 포함되어 있습니다.

<UserControl x:Class="WPF_ParsingXML.View.FileSubPages.UserControls.FileInitialListup"
             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:WPF_ParsingXML.View.FileSubPages.UserControls"
             xmlns:bp="clr-namespace:WPF_ParsingXML.ViewModels.Base"
             mc:Ignorable="d"
             d:DesignHeight="450"
             d:DesignWidth="500">
    <UserControl.Resources>
        <bp:BindingProxy x:Key="proxy" Data="{Binding}" />
        <Style x:Key="CheckBoxOneClick" />
    </UserControl.Resources>

    <DataGrid Name="fileList"
              ItemsSource="{Binding Files}"
              HorizontalAlignment="Center"
              HorizontalContentAlignment="Center"
              VerticalAlignment="Center"
              AutoGenerateColumns="False"
              IsReadOnly="True"
              ScrollViewer.HorizontalScrollBarVisibility="Disabled">
        <DataGrid.Columns>
            <DataGridCheckBoxColumn x:Name="xCheckBox"
                                    Binding="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                    ElementStyle="{StaticResource CheckBoxOneClick}"
                                    Width="30"
                                    MinWidth="30">
                <DataGridCheckBoxColumn.Header>
                    <CheckBox x:Name="xSelectAll"
                              IsChecked="{Binding Data.Files.IsSelectAll, Source={StaticResource proxy} }" />
                </DataGridCheckBoxColumn.Header>
            </DataGridCheckBoxColumn>
            <DataGridTextColumn Header="FileName"
                                Binding="{Binding FileName}"
                                Width="1*" />
            <DataGridTextColumn Header="Modified Date"
                                Binding="{Binding Modified}"
                                Width="1*" />         
            <DataGridTextColumn Header="Size"
                                Binding="{Binding Size, StringFormat={}{0} MB}"
                                Width="1*" />        
        </DataGrid.Columns>
    </DataGrid>
</UserControl>

아래는 VM입니다.

namespace WPF_ParsingXML.ViewModels
{
    public class FileVM : INotifyPropertyChanged
    {
        #region Propertites

        private IXMLHelper _XMLHelper;

        public ICommand fileSearchCommand { get; private set; }
        public ICommand resetCommand { get; private set; }

        public List<Filtering> filterings { get; set; }
        public SelectableCollection<File> Files { get; set; } = new SelectableCollection<File>();
        private Filtering _selectedFiltering;
        public Filtering SelectedFiltering
        {
            get { return _selectedFiltering; }
            set
            {
                _selectedFiltering = value;
                Files.Clear();
                OnPropertyChanged(nameof(SelectedFiltering));
            }
        }
        private SelectableCollection<File> _selectedFiles;
        public SelectableCollection<File> SelectedFiles
        {
            get { return _selectedFiles; }
            set
            {
                _selectedFiles = value;
                OnPropertyChanged(nameof(SelectedFiles));
            }
        }        
        private int _selectedTabIndex = 0;
        public int SelectedTabIndex
        {
            get { return _selectedTabIndex; }
            set { _selectedTabIndex = value; OnPropertyChanged(nameof(SelectedTabIndex)); }
        }
        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        public FileVM()
        {
            InitProcess();
        }

        private void InitProcess()
        {
            _downloadHelper = new DownloadHelper();
            _XMLHelper = new XMLHelper();

            fileSearchCommand = new RelayCommand<object>(GetFileList);
            resetCommand = new RelayCommand<object>(ResetInfo);
            filterings = new List<Filtering>();
            CreateFiltering();
        }

        private void CreateFiltering()
        {
            filterings.Add(new Filtering { ModelName = "Book" });
            filterings.Add(new Filtering { ModelName = "Article" });
            filterings.Add(new Filtering { ModelName = "News" });
        }

        public event PropertyChangedEventHandler PropertyChanged;
        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        public void GetFileList(object filter)
        {
            if (Files.Count != 0)
            {
                return;
            }

            //IsClickedSearch = false;
            var filtering = filter as Filtering;
            var intialListFile = _XMLHelper.ParseXML_Sample2();
            foreach (var sw in intialListFile)
            {
                Files.Add(sw);
            }
        }
        private void ResetInfo(object ojb)
        {
            SelectedTabIndex = 0;

            // Clean up: Freezable로 인해 Files가 초기화 안됨.
            Files = null;
            SelectedFiles = null;
            FolderPath = null;
        }
        
    }
}

문제가 발생한 부분은 FileInitialListup UserControl에서 사용된 Freezable 때문이었습니다. 일전에 <CheckBox x:Name="xSelectAll" IsChecked="{Binding Data.Files.IsSelectAll, Source={StaticResource proxy} }" /> TabControl에서 RelativeSource FindAncestor모드가 적용이 되지 않았습니다. 그래서 관련 자료를 찾다가 Freezable 알게 되서 우선 적용해보았습니다.

using System.Windows;

namespace WPF_ParsingXML.ViewModels.Base
{
    public class BindingProxy : Freezable
    {
        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        public object Data
        {
            get { return (object)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        public static readonly DependencyProperty DataProperty =
            DependencyProperty.Register("Data", typeof(object),
            typeof(BindingProxy), new UIPropertyMetadata(null));
    }
}

하지만 이로 인해, VM에서 Reset버튼으로 초기화해도 기존 정보를 갖고 있게 되는 것입니다. VM에서 정보가 변경되면 Freezable에서 업데이트를 해주면 좋을 것 같은데… 조언을 부탁드립니다.

2개의 좋아요

바인딩된 View가 계속 존재하므로 View에서 ViewModel의 초기화된 결과를 반영하려면 변화된 것을 감지해야 합니다.

ResetInfo() 메소드로 호출로 View에 반영시키려면 두가지 방법이 있습니다.

  1. Files 속성을 반응형으로 변경
    Files 속성 역시 속성 setOnPropertyChanged(nameof(Files)) 넣어줍니다.

  2. ResetInfo()OnPropertyChanged(nameof(Files)) 삽입

4개의 좋아요

@dimohy 말씀해주신대로 수행하니 정상적으로 동작합니다. 조언 감사드립니다!

2개의 좋아요