lsirikh / ProperyExplorerTest

에로로님의 예제를 구현 및 응용해보자
0 stars 0 forks source link

> 1. `public class PanningStructure : DependencyObject` 을 따로 선언해서 사용하시는 이유는 무엇일까요? #8

Open lsirikh opened 2 years ago

lsirikh commented 2 years ago
  1. public class PanningStructure : DependencyObject 을 따로 선언해서 사용하시는 이유는 무엇일까요? 분석해보니까, public class InCanvasMovingBehavior : DependencyObject 여기 등록된 inner class고 주로 기능적인 측면을 담당하는 클래스 같았습니다.

원래는 PanningStructure 역시 관리 대상으로 만들었습니다. 그래서 InCanvasMovingBehavior 에서 AP 로 get/set 하기 위해 DO 를 상속받은 거였는데, 생각해보니 딱히 관리가 필요 없을 거 같아서 InCanvasMovingBehavior 의 AP 를 삭제했고, PanningStruture 의 DO 상속은 제거하는 걸 빠뜨린 겁니다. DO 상속은 삭제해도 됩니다.(물론 그냥 둬도 됩니다.) PanningStruture 가 DO 에 의존적인 기능을 사용하지 않으니까 상관없습니다. 나중에 관리가 필요해 지면 그 때 다시 붙이면 되니까요.

  1. 들어오는 ViewModel에 따른 PanningStructure를 생성하고, InCavasMovingBehavior 단에는 Xaml에 연동할 AP쪽을 세팅하는 역할을 하는 것 같앗습니다.

렬루? 오해하시면 안 되는 게, InCavasMovingBehavior 역시 view 의 일부분입니다. 여기서 ViewModel 에 대한 접근은 MVVM 을 깨는 행위예요. 물론 인터페이스를 이용한 느슨한 결합으로 접근할 수도 있지만 엄격한 MVVM 구현 차원에서는 권장하지 않아요. 다시 확인해 보시죵. 진짜 ViewModel 인지.

그래서 생각을 해봤습니다. 평소에는 PropertyExplorer 창이 안나타나다가, 캔버스 위에 Rectangle 혹은 Ellipse 등 오른쪽 마우스 버튼을 누르면 메뉴가 나오고 메뉴를 클릭하면 창이 나타나는 구조를 구현하는 것 가능할까요?

당연히 가능하겠죠? 구현하는 방법은 많아요. 선택은 만드는 사람이 결정할 문제이지요.

  1. 캔버스를 클릭하면 선택된 ListBoxItem이 전체 UnSelected 형태로 만드려면 어디에 Command를 넣어주면 될까요? RoutedCommand를 통해서 myListBox.UnselectAll(); 해주려고 하는데요. 아무래도 Scrollviewer에 걸리는데 어떻게 MVVM으로 처리해주면 좋을까요?

전 개인적으로 엄격한 MVVM 패턴 구현 시 RountedCommand 추천하지 않아요. 구현체가 PresentationCore 에 정의되어 있거든요. RountedCommand 를 CustomControl 등, view 간 연결에서 사용하는 건 상관없지만, 어차피 ViewModel 에서는 사용 못하기 떄문에 쓸 수도 없고 쓰는 걸 권장하지도 않아요. Command 가 왜 필요한 지 이해하면 제 말이 이해가 될 겁니다.

요거는 제가 리뷰에 댓글 달아놓은 걸 확인하시면 될 듯... =ㅂ=

Originally posted by @shwlee in https://github.com/lsirikh/ProperyExplorerTest/issues/7#issuecomment-933157760

lsirikh commented 2 years ago

감사합니다.

일단 확인해보곡 있는데요. 어렵네요...ㅠㅠ

최대한 해보고 다시 PR남기도록 하겟습니다.

너무 감사합니다. 쉬시는날에 고생 많으셨습니다~

lsirikh commented 2 years ago

안녕하세요.

또 문의드릴거 한 개 남깁니다.

잘 이해가 안되는게 있어서요.

MainContainerViewModel에

public class MainContainerViewModel : BaseViewModel
   {`
       private RelayCommand<IPropertyOperative> _selectModelCommand;
       public ICommand SelectModelCommand { get; set; }

     public MainContainerViewModel()
       {`
                 //canExecute는 제가 첨가 했습니다.
                  //SelectModelCommand = this._selectModelCommand ?? (this._selectModelCommand = new RelayCommand<IPropertyOperative>(this.SelectModel, canExecute));
SelectModelCommand = new RelayCommand<IPropertyOperative>(execute, canExecute);
....

        private void execute(IPropertyOperative model)
        {
            this.PropertyExplorer.SelectModel(model);
        }

        private bool canExecute(IPropertyOperative arg)
        {
            //return true;
            Debug.WriteLine($"canExcute : {arg}, shape : {this._selectedShape}");

            //return true;
            if (this._selectedShape != null)
            {
                Debug.WriteLine("return : true");
                return true;
            }
            else
            {
                Debug.WriteLine("return : false");

                return false;
            }

        }

이렇게 만들었고요.

MainContainer에

                    <ListBox.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="Property" Command="{Binding SelectModelCommand}" 
                                      CommandParameter="{Binding SelectedShape}"/>
                        </ContextMenu>
                    </ListBox.ContextMenu>

이렇게 해줬는데요.

원래 RelayCommand가 canExecute를 계속 검사하다가 조건이 되면 execute하는거로 알고 있거든요.

근데 제가 Canvas를 클릭하면 _selectedShape 이게 null이 되게(정확하게 ListBox.Unselectall()) 코드 비하인드에 넣어놨는데요. _selectedShape 이게 null은 확인했는데요. 문제는 canExecute가 계속 해서 실행되는게 아닌가봐요.

한 값으로 고정되버려요. true든 false든 갱신이 안되는 느낌입니다.

혹시 알 수 있을까요?

shwlee commented 2 years ago

원래 RelayCommand가 canExecute를 계속 검사하다가 조건이 되면 execute하는거로 알고 있거든요.

진짜 그런 지 확인을 한 번 해보세요. 알고 있는 게 틀린 것일 수 있잖아요. -ㅁ-;;; (확실하지 않으면 일단 검색과 공부 ㄱㄱ)

요거는 따로 검색을 해보시면 어렵지 않게 알 수 있을텐데요. CanExecute 는 기본적으로 ICommand 인터페이스의 CanExecuteChanged 이벤트가 발생하면 그 때 호출되는 겁니다. (계속 감시하는 게 아니에요.) 그럼 CanExecuteChanged 는 언제 발생하느냐. https://social.msdn.microsoft.com/Forums/sqlserver/en-US/40ec8aeb-6ceb-4ce2-a050-d9309642240c/when-does-wpf-call-the-command-canexecute?forum=wpf 요기 보면 대충 설명되어 있는데 더 자세한 문서는 지금 못 찾겠네요. 대략 view 의 변화가 일어날만한 위치에서 발생한다고 보면 됩니다. 주로 mouse 액션에 의한 포커스 이동 따위가 그렇죠. 그러다 보니 이게 의도한대로 발생하지 않을 수도 있어요. 그래서 특별한 동작에서 강제로 호출할 수도 있지만 일단 여기서는 그 문제가 아닌거 같으니까 패쓰... (https://blog.naver.com/vactorman/80174271876) 요거 한 번 훑어보세욜..

shwlee commented 2 years ago

그리고 ContextMenu(Menu, MenuItem , etc) 와 Popup 컨트롤은 좀 특별한 녀석들이에욤. 얘네는 로딩될 때 VisualTree 에 편입되는 게 아니라, 자기네가 렌더링될 때 VisualTree 에 편입되는 애들이어요. 그래서 전체가 로딩되는 것을 전체로 binding 구문을 만들면 동작하지 않을 가능성이 높은 애들이에요.

요것두 한 번 찾아보세염. 얘네한테 binding 하는 방법 찾으면 대충 답 나올거에요. ~ㅂ~

lsirikh commented 2 years ago

답변 감사드립니다.

일단 안되서 제가 별도로 아래와 같은 Command를 만들었는데요.

public class SelectItemCommand<IPropertyOperative> : ICommand
    {

        MainContainerViewModel VM; // 아마 여기가 Loosely-Coupled 원칙을 훼손하지 않나싶네요...

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

        public SelectItemCommand(MainContainerViewModel vm)
        {
            VM = vm; // 아마 여기가 Loosely-Coupled 원칙을 훼손하지 않나싶네요...
        }

        public bool CanExecute(object parameter)
        {
            if (parameter != null)
                return true;
            else
                return false;
        }

        public void Execute(object parameter)
        {
            VM.SelectModel((Defines.Interfaces.IPropertyOperative)parameter);
        }
    }

이렇게 작성해서 해결을 했거든요...

아마 그전에 ViewModel에서 작동안된게 혹시 이것 때문일까요?

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

이 코드가 없어서 ??

보내주신 내용을 다시 공부해보겠습니다.

답변 감사합니다.ㅠㅠ

shwlee commented 2 years ago

CanExecute 가 호출이 안 된다는 건가요? 호출은 되는데 오동작한다는 건가요? 개인적으로 굳이 SelectItemCommand 를 추가할 필요를 못느끼겠는데... 기존 RelayCommand 로는 할 수 없는 별도의 동작이 필요한건가요? 상황을 명확하게 정리할 필요가 있어보입니다. (작성하신 코드를 보니 대충 짐작은 가는데 정확히 뭘 하고 싶은 건지, 뭐가 궁금한 건지는 잘 모르겠어요.)

lsirikh commented 2 years ago

두서가 없이 정리가 되서 질문을 드린것 같습니다.

좀 더 이해하시 쉽게 정리해서 올려보겠습니다.

다시 정리해서 말씀드리면 제가 현재 하고 싶은게,

  1. Canvas 상의 오브젝트를 오른쪽 마우스 버튼으로 메뉴를 띄우고 Property란 항목을 클릭하면, PropertyExplorer창이 나타나게 한다.
  2. Canvas 상에 오브젝트가 아닌 Canvas 바탕화면을 클릭하면 ListBoxItem이 모두 Unselected 되게 한다.
  3. 마우스로 Canvas 바탕화면을 오른쪽 버튼을 클릭하면 Property란 항목이 비활성화로 나타나게 한다.

오늘 작업해본건 총 3가지 입니다.

1, 3번은 제가 위에서 보여드린 public class SelectItemCommand : ICommand 를 이용해서 해결했습니다.

전문을 다시 보면, SelectItemCommand.cs

public class SelectItemCommand<IPropertyOperative> : ICommand
    {

        MainContainerViewModel VM;

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

        public SelectItemCommand(MainContainerViewModel vm)
        {
            VM = vm;
        }

        public bool CanExecute(object parameter)
        {
            if (parameter != null)
                return true;
            else
                return false;
        }

        public void Execute(object parameter)
        {
            //IPropertyOperative param = parameter as IPropertyOperative;
            VM.SelectModel((Defines.Interfaces.IPropertyOperative)parameter);
        }
    }

MainContainerViewModel.cs

public class MainContainerViewModel : BaseViewModel
    {

        ...중략...
        public SelectItemCommand<IPropertyOperative> SelectModelCommand { get; set; }

        /// <summary>
        /// ItemControl의 ItemSource로 활용되는 ObservableCollection 변수이다.
        /// BaseShapeViewModel의 Get할때, 새로운 인스턴스로 넘긴다.
        /// </summary>
        public ObservableCollection<BaseShapeViewModel> Items { get; } = new ObservableCollection<BaseShapeViewModel>();

        private BaseShapeViewModel _selectedShape;

        public BaseShapeViewModel SelectedShape
        {
            get => _selectedShape;
            set
            {
                this.SetProperty(ref this._selectedShape, value);
                Debug.WriteLine($"SelectedShape: {_selectedShape}");
                //this.SelectModel(this._selectedShape);
            }
        }

        public PropertyExplorerViewModel PropertyExplorer { get; set; } = new PropertyExplorerViewModel();

        /// <summary>
        /// 생성자를 통한 초기화
        /// 사각형, 타원(원), 선을 초기에 등록한다.
        /// </summary>
        public MainContainerViewModel()
        {

            SelectModelCommand = new SelectItemCommand<IPropertyOperative>(this);

            Random rand = new Random();
            var min = 1;
            var max = 600;
            var rectModel = new RectModel((double)rand.Next(min, max), (double)rand.Next(min, max));
            var ellipseModel = new EllipseModel();
            var lineModel = new LineModel((double)rand.Next(min, max), (double)rand.Next(min, max));
            this.Items.Add(new RectViewModel(rectModel));
            this.Items.Add(new EllipseViewModel(ellipseModel));
            this.Items.Add(new LineViewModel(lineModel));

        }

        /// <summary>
        /// Model 선택에 따른 해당 모델에 설정된 속성 정보를 
        /// Property를 PropertyChange를 위한 목록에 등록하는 과정
        /// </summary>
        /// <param name="model"></param>
        public void SelectModel(IPropertyOperative model)
        {
            if(model != null)
                this.PropertyExplorer.SelectModel(model);
        }
    }

이렇게 ViewModel에 넣었구요.

MainContainer.xaml

...생략...
<ListBox x:Name ="CanvasListBox"
                    Background="Transparent"
                    ItemsSource="{Binding Items}"
                    SelectionMode="Extended"
                    SelectedItem="{Binding SelectedShape, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
                    Margin="5"
                    >

                    <ListBox.ContextMenu>
                        <ContextMenu>
                            <MenuItem x:Name="Property" Header="Property" Command="{Binding SelectModelCommand}" 
                                      CommandParameter="{Binding SelectedShape}"/>
                        </ContextMenu>
                    </ListBox.ContextMenu>
...생략...

이렇게 해서 구현했습니다.

이렇게 구현해도 괜찮은건지 여쭤보고 싶었고요.

또 한가지는 기존의 MainContainerViewModel.cs에

this._selectModelCommand ?? (this._selectModelCommand = new RelayCommand<IPropertyOperative>(this.SelectModel));
this._selectModelCommand ?? (this._selectModelCommand = new RelayCommand<IPropertyOperative>(this.SelectModel, canExecute));

로 바꾸고 구현을 해봤습니다. 그런데 다음과 같은 문제가 발생했습니다.

발생문제) 처음 프로그램 실행 후, Canvas위의 오브젝트를 클릭하거나 , 바탕화면을 클릭하는 순간 그 결과가 고정이 되버려서 (null, 혹은 RectViewModel 값) 뭔가 OnPropertyChange같이 갱신이 안되는 현상이 나타났습니다.

해결책) 제가 생각한 해결책은 'SelectItemCommand.cs'를 만드는 것이었습니다.

열심히 정리해봤는데 혹시 이해가 되실까요?

shwlee commented 2 years ago

여기에 코드별로 댓글다는 게 어려우니 테스트 해본 것들을 PR 로 보내주시면 한 번 볼게요

lsirikh commented 2 years ago

다시 변경해서 보내드리겠습니다~