c# - 패턴 - wpf youtube




ViewModel은 어떻게 폼을 닫아야합니까? (18)

WPF 및 MVVM 문제를 배우려고하지만 방해가됩니다. 이 질문은 비슷하지만 비슷합니다 (handling-dialog-in-wpf-with-mvvm) ...

MVVM 패턴을 사용하여 작성된 "로그인"양식이 있습니다.

이 양식에는 일반 데이터 바인딩을 사용하여 XAML의보기에 바인딩되는 사용자 이름과 암호를 보유하는 ViewModel이 있습니다. 또한 폼의 "로그인"버튼, 일반적인 데이터 바인딩을 사용하는 agan에 바인딩 된 "로그인"명령을 가지고 있습니다.

"Login"명령이 실행되면 ViewModel에서 꺼지고 네트워크를 통해 데이터를 보내서 로그인하는 기능을 호출합니다.이 기능이 완료되면 다음 두 가지 작업이 수행됩니다.

  1. 로그인이 잘못되었습니다. MessageBox 만 표시하고 모두 괜찮습니다.

  2. 로그인이 유효했습니다. 로그인 양식을 닫고 DialogResult 로서 true를 반환해야 DialogResult .

문제는 ViewModel이 실제 뷰에 대해 아무것도 모르는 것이므로 뷰를 닫고 특정 DialogResult를 반환하도록 알리는 방법은 무엇입니까 ?? CodeBehind에 코드를 붙이거나 ViewModel을 통해 View를 전달할 수는 있지만 MVVM 전체를 완전히 뒤집을 수있는 것처럼 보입니다.

최신 정보

결국 MVVM 패턴의 "순도"를 위반하고 View에서 Closed 이벤트를 게시하고 Close 메서드를 노출합니다. 그러면 ViewModel은 view.Close 호출 view.Close . 뷰는 인터페이스를 통해서만 인식되며 IOC 컨테이너를 통해 유선 연결되므로 테스트 가능성이나 유지 보수성이 손실되지 않습니다.

받아 들여진 응답이 -5 표에 오히려 어리석은 것처럼 보인다! "순수한"문제를 해결하면서 좋은 감정을 잘 알고 있지만 분명히 한 줄 방법을 피하기 위해 200 줄의 사건, 명령 및 행동을 생각하는 유일한 사람은 아닙니다. "패턴"과 "순결"의 이름은 약간 우습다.


Joe White의 솔루션을 구현했지만 때때로 " DialogResult는 Window가 만들어지고 대화 상자로 표시된 후에 만 ​​설정할 수 있습니다 "라는 오류가 발생했습니다.

View가 닫힌 후에 ViewModel을 유지하고 있었고 때때로 같은 VM을 사용하여 나중에 새 View를 열었습니다. 이전 View가 가비지 수집되기 전에 새 View를 닫으면 DialogResultChanged 가 닫힌 창에 DialogResult 속성을 설정하려고 시도하여 오류를 유발합니다.

내 솔루션은 DialogResultChanged 를 변경하여 창 IsLoaded 속성을 확인하는 것이 었습니다.

private static void DialogResultChanged(
    DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    var window = d as Window;
    if (window != null && window.IsLoaded)
        window.DialogResult = e.NewValue as bool?;
}

이 변경 후 닫힌 대화 상자에 대한 첨부 파일은 무시됩니다.


ViewModel에서 View가 등록 할 이벤트를 노출하게 할 수 있습니다. 그런 다음 ViewModel이보기를 닫을 시간을 결정하면보기를 닫는 해당 이벤트를 시작합니다. 특정 결과 값을 다시 전달하려면 ViewModel에 속성 값이 있어야합니다.


나는 Thejuan의 답변 에 의해 더 단순한 첨부 된 속성을 쓰는 것에 영감을 받았다 . 스타일 없음, 트리거 없음; 대신 다음과 같이하면됩니다.

<Window ...
        xmlns:xc="clr-namespace:ExCastle.Wpf"
        xc:DialogCloser.DialogResult="{Binding DialogResult}">

이것은 WPF 팀이 올바르게 만든 것처럼 DialogResult를 종속 속성으로 만들었던 것처럼 깨끗합니다. 그냥 bool? DialogResult 넣어 bool? DialogResult bool? DialogResult 속성을 사용하여 INotifyPropertyChanged 및 voilà을 구현하면 ViewModel은 속성을 설정하여 Window를 닫고 DialogResult를 설정할 수 있습니다. MVVM이어야합니다.

DialogCloser의 코드는 다음과 같습니다.

using System.Windows;

namespace ExCastle.Wpf
{
    public static class DialogCloser
    {
        public static readonly DependencyProperty DialogResultProperty =
            DependencyProperty.RegisterAttached(
                "DialogResult",
                typeof(bool?),
                typeof(DialogCloser),
                new PropertyMetadata(DialogResultChanged));

        private static void DialogResultChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            var window = d as Window;
            if (window != null)
                window.DialogResult = e.NewValue as bool?;
        }
        public static void SetDialogResult(Window target, bool? value)
        {
            target.SetValue(DialogResultProperty, value);
        }
    }
}

나는 또한 이것을 나의 blog에 게시했다 .


나는 모든 대답을 읽었지 만, 나는 그 중 대부분이 충분하지 못하거나 악화되고 있다고 말해야한다.

대화 상자 창을 표시하고 대화 상자 결과를 반환하는 역할을하는 DialogService 클래스를 사용 하여이 문제훌륭하게 처리 할 수 ​​있습니다. 구현 및 사용법을 보여주는 샘플 프로젝트를 만들었습니다.

다음은 가장 중요한 부분입니다.

//we will call this interface in our viewmodels
public interface IDialogService
{
    bool? ShowDialog(object dialogViewModel, string caption);
}

//we need to display logindialog from mainwindow
public class MainWindowViewModel : ViewModelBase
{
    public string Message {get; set;}
    public void ShowLoginCommandExecute()
    {
        var loginViewModel = new LoginViewModel();
        var dialogResult = this.DialogService.ShowDialog(loginViewModel, "Please, log in");

        //after dialog is closed, do someting
        if (dialogResult == true && loginViewModel.IsLoginSuccessful)
        {
            this.Message = string.Format("Hello, {0}!", loginViewModel.Username);
        }
    }
}


public class DialogService : IDialogService
{
    public bool? ShowDialog(object dialogViewModel, string caption)
    {
        var contentView = ViewLocator.GetView(dialogViewModel);
        var dlg = new DialogWindow
        {
            Title = caption
        };
        dlg.PART_ContentControl.Content = contentView;

        return dlg.ShowDialog();
    }
}

이것은 단순하지 않습니까? EventAggregator 또는 다른 유사한 솔루션보다 디버깅하기가 쉽고, 읽기 쉽고, 마지막으로 읽기 쉽습니다.

당신이 볼 수 있듯이, 내보기 모델에서 나는 여기에 내 게시물에 설명 된 ViewModel 첫 번째 접근 방식을 사용했습니다 : WPF에서 ViewModel에서 View를 호출하기위한 모범 사례

물론 DialogService.ShowDialog 에는 대화 상자를 구성하는 데 필요한 옵션이 있어야합니다 (예 : 단추와 명령을 실행해야 함). 이렇게 다른 방법이 있지만 그 범위를 벗어났습니다 :)



또 다른 해결책은 DialogResult와 같은 View Model에서 INotifyPropertyChanged를 사용하여 속성을 생성 한 다음 Code Behind에서 다음과 같이 작성하는 것입니다.

public class SomeWindow: ChildWindow
{
    private SomeViewModel _someViewModel;

    public SomeWindow()
    {
        InitializeComponent();

        this.Loaded += SomeWindow_Loaded;
        this.Closed += SomeWindow_Closed;
    }

    void SomeWindow_Loaded(object sender, RoutedEventArgs e)
    {
        _someViewModel = this.DataContext as SomeViewModel;
        _someViewModel.PropertyChanged += _someViewModel_PropertyChanged;
    }

    void SomeWindow_Closed(object sender, System.EventArgs e)
    {
        _someViewModel.PropertyChanged -= _someViewModel_PropertyChanged;
        this.Loaded -= SomeWindow_Loaded;
        this.Closed -= SomeWindow_Closed;
    }

    void _someViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == SomeViewModel.DialogResultPropertyName)
        {
            this.DialogResult = _someViewModel.DialogResult;
        }
    }
}

가장 중요한 부분은 _someViewModel_PropertyChanged 입니다. DialogResultPropertyNameSomeViewModel public const string입니다.

ViewModel에서 수행하기 어려운 경우에 대비하여 뷰 컨트롤에서 일부 변경을 수행하기 위해 이러한 종류의 트릭을 사용합니다. ViewModel에서 OnPropertyChanged를 사용하면 View에서 원하는 모든 작업을 수행 할 수 있습니다. ViewModel은 여전히 ​​'단위 테스트 가능'이며 코드 뒤에있는 코드의 일부 작은 부분은 차이가 없습니다.


방대한 수의 답변에 추가하려면 다음을 추가하고 싶습니다. ViewModel에 ICommand가 있다고 가정하고 해당 명령을 사용하여 창 (또는 해당 작업에 대한 다른 작업)을 닫으려면 다음과 같은 것을 사용할 수 있습니다.

var windows = Application.Current.Windows;
for (var i=0;i< windows.Count;i++ )
    if (windows[i].DataContext == this)
        windows[i].Close();

완벽하지는 않으며 테스트하기가 어려울 수 있습니다 (고정적으로 조롱하거나 스텁하기가 어려울 수 있음). 그러나 다른 솔루션보다 깨끗합니다 (IMHO).

에릭


여기에 내가 처음에했던 일이있다.하지만 그 일은 오래 가지 못하고 추한 것 같다. (전역 정적 인 것은 결코 좋지 않다.)

1 : App.xaml.cs

public partial class App : Application
{
    // create a new global custom WPF Command
    public static readonly RoutedUICommand LoggedIn = new RoutedUICommand();
}

2 : LoginForm.xaml

// bind the global command to a local eventhandler
<CommandBinding Command="client:App.LoggedIn" Executed="OnLoggedIn" />

3 : LoginForm.xaml.cs

// implement the local eventhandler in codebehind
private void OnLoggedIn( object sender, ExecutedRoutedEventArgs e )
{
    DialogResult = true;
    Close();
}

4 : LoginFormViewModel.cs

// fire the global command from the viewmodel
private void OnRemoteServerReturnedSuccess()
{
    App.LoggedIn.Execute(this, null);
}

나중에이 코드를 모두 제거하고 LoginFormViewModel 호출하여 Close 메서드를 호출했습니다. 결국 훨씬 더 친절하고 쉽게 따라갈 수있었습니다. IMHO 패턴의 요점은 사람들에게 앱이하는 일을 이해하는 더 쉬운 방법을 제공하는 것입니다.이 경우 MVVM은 내가 사용하지 않은 것보다 이해하기가 훨씬 어려워졌으며 이제는 반 패턴 입니다.


왜 그냥 명령 매개 변수로 창을 전달하지?

기음#:

 private void Cancel( Window window )
  {
     window.Close();
  }

  private ICommand _cancelCommand;
  public ICommand CancelCommand
  {
     get
     {
        return _cancelCommand ?? ( _cancelCommand = new Command.RelayCommand<Window>(
                                                      ( window ) => Cancel( window ),
                                                      ( window ) => ( true ) ) );
     }
  }

XAML :

<Window x:Class="WPFRunApp.MainWindow"
        x:Name="_runWindow"
...
   <Button Content="Cancel"
           Command="{Binding Path=CancelCommand}"
           CommandParameter="{Binding ElementName=_runWindow}" />

이 방법으로 viewmodel을 통해이 작업을 수행하는 방법에 대한 질문에 답변하지는 않지만 XAML + 블렌드 SDK 만 사용하여이를 수행하는 방법을 보여줍니다.

블렌드 SDK에서 두 파일을 다운로드하여 사용했습니다. 두 파일 모두 Microsoft에서 NuGet을 통해 패키지로 제공 할 수 있습니다. 파일은 다음과 같습니다.

System.Windows.Interactivity.dll 및 Microsoft.Expression.Interactions.dll

Microsoft.Expression.Interactions.dll은 속성을 설정하거나 viewmodel 또는 다른 대상에서 메서드를 호출하는 기능과 같은 훌륭한 기능을 제공하며 다른 위젯도 포함합니다.

일부 XAML :

<Window x:Class="Blah.Blah.MyWindow"
    ...
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
  ...>
 <StackPanel>
    <Button x:Name="OKButton" Content="OK">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="True"
                      IsEnabled="{Binding SomeBoolOnTheVM}" />                                
          </i:EventTrigger>
    </Button>
    <Button x:Name="CancelButton" Content="Cancel">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="False" />                                
          </i:EventTrigger>
    </Button>

    <Button x:Name="CloseButton" Content="Close">
       <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <!-- method being invoked should be void w/ no args -->
                    <ei:CallMethodAction
                        TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                        MethodName="Close" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
    </Button>
 <StackPanel>
</Window>

단순한 OK / Cancel 동작 만 수행하려는 경우 Window가 Window.ShowDialog ()로 표시되어있는 한 IsDefault 및 IsCancel 속성을 사용하여 w / 사용을 해제 할 수 있습니다.
개인적으로 IsDefault 속성을 true로 설정 한 단추가있는 문제가 있었지만 페이지가로드 될 때 숨겨졌습니다. 위 그림과 같이 멋지게 연출한 것처럼 보이지 않으므로 위의 그림과 같이 Window.DialogResult 속성을 설정하기 만하면됩니다.


이것은 아마도 매우 늦었을 것입니다.하지만 같은 문제가 생겨서 저에게 맞는 솔루션을 발견했습니다.

대화 상자가없는 응용 프로그램을 만드는 방법을 알아낼 수 없습니다 (어쩌면 단지 마음 블록 일 수도 있습니다). 그래서 MVVM과 대화를 보여주는 난관에 처해있었습니다. 그래서이 CodeProject 기사를 보았습니다.

http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

기본적으로 창을 다른 창 (xaml에서는 허용되지 않음)의 시각적 트리 내에있게하는 UserControl입니다. 또한 IsShowing이라는 부울 DependencyProperty를 노출합니다.

resourcedictionary에서 일반적으로 컨트롤의 Content 속성! = null via triggers를 통해 대화 상자를 표시하는 스타일을 설정할 수 있습니다.

<Style TargetType="{x:Type d:Dialog}">
    <Style.Triggers>
        <Trigger Property="HasContent"  Value="True">
            <Setter Property="Showing" Value="True" />
        </Trigger>
    </Style.Triggers>
</Style>

대화 상자를 표시하려는보기에서 다음과 같이하십시오.

<d:Dialog Content="{Binding Path=DialogViewModel}"/>

그리고 ViewModel에서해야 할 일은 속성을 값으로 설정하는 것입니다 (참고 : ViewModel 클래스는 발생한 일을 알기 위해 뷰에 대해 INotifyPropertyChanged를 지원해야합니다).

이렇게 :

DialogViewModel = new DisplayViewModel();

ViewModel을 뷰와 일치 시키려면 resourcedictionary에서 이와 같은 것이 있어야합니다 :

<DataTemplate DataType="{x:Type vm:DisplayViewModel}">
    <vw:DisplayView/>
</DataTemplate>

이 모든 것들을 통해 한 줄짜리 코드가 대화 상자를 보여줍니다. 문제는 위의 코드로는 대화 상자를 실제로 닫을 수 없다는 것입니다. 따라서 DisplayViewModel이 위의 코드 대신 상속하는 ViewModel 기본 클래스에 이벤트를 넣어야하는 이유는 다음과 같습니다

        var vm = new DisplayViewModel();
        vm.RequestClose += new RequestCloseHandler(DisplayViewModel_RequestClose);
        DialogViewModel = vm;

그런 다음 콜백을 통해 대화 상자의 결과를 처리 할 수 ​​있습니다.

이것은 다소 복잡해 보일지 모르지만 일단 기초 작업이 완료되면 매우 간단합니다. 다시 이것은 내 구현이다. 나는 다른 것들이있을 것이라고 확신한다. :)

희망이 도움이, 그것은 나를 구 했어요.


참고로, 나는이 똑같은 문제에 부딪 쳤고, 최선의 대답이 아닐지라도 전역 또는 정적을 필요로하지 않는 해결 방법을 찾았다 고 생각합니다. 너희들이 스스로 결정하도록 내버려둬.

필자의 경우 ViewModel은 ViewModelMain을 호출 할 수있는 창을 인스턴스화하여 LoginFormViewModel을 알기도합니다 (위의 상황을 예제로 사용).

그래서 내가 한 것은 ICommand 유형의 LoginFormViewModel에 속성을 만드는 것입니다 (CloseWindowCommand라고합니다). 그런 다음 Window에서 .ShowDialog ()를 호출하기 전에 LoginFormViewModel의 CloseWindowCommand 속성을 인스턴스화 한 Window의 window.Close () 메서드로 설정했습니다. 그런 다음 LoginFormViewModel 내부에서 CloseWindowCommand.Execute ()를 호출하여 창을 닫습니다.

그것은 약간의 임시 해결책 / 해킹입니다.하지만 실제로 MVVM 패턴을 깨지 않고 잘 작동합니다.

당신이 좋아하는만큼이 과정을 비판해라. 나는 그것을 가져갈 수있다! :)


Ok,이 질문은 거의 6 살이기 때문에 나는 그것이 적절한 대답이라고 생각하는 것을 여기에서 찾을 수 없다. 그래서 나는 내 "2 센트"를 나눌 수있다.

나는 실제로 그것을하는 2 가지 방법이 있습니다, 첫번째 것은 간단한 것입니다 ... 맞은 것의 두번째 것, 그래서 당신이 맞은 것을 찾고 있다면, # 1을 건너 뛰고 # 2로 점프하십시오 :

1. 빠르고 쉬운 (하지만 완전하지는 않음)

작은 프로젝트가있는 경우 때때로 ViewModel에서 CloseWindowAction 을 만듭니다.

        public Action CloseWindow { get; set; } // In MyViewModel.cs

View를 작성하거나 View의 코드에서 작성한 메소드는 Action이 호출 할 메소드를 설정합니다.

(MVVM은 View와 ViewModel을 구분한다는 것을 기억하십시오. View의 코드는 여전히 View이며 적절한 분리가 이루어지면 패턴을 위반하지 않습니다)

일부 ViewModel이 새 창을 만드는 경우 :

private void CreateNewView()
{
    MyView window = new MyView();
    window.DataContext = new MyViewModel
                             {
                                 CloseWindow = window.Close,
                             }; 
    window.ShowDialog();
}

또는 기본 창에서 원하는 경우보기의 생성자 아래에 배치하십시오.

public MyView()
{
    InitializeComponent();           
    this.DataContext = new MainViewModel
                           {
                                CloseWindow = this.Close
                           };
}

창을 닫으려면 ViewModel에서 Action을 호출하면됩니다.

2. 올바른 방법

이제 올바른 방법은 Prism (IMHO)을 사용하는 것입니다. 모든 내용은 여기에서 찾을 수 있습니다 .

상호 작용 요청을 만들고, 새 창에서 필요한 데이터로 채우고, 점심을 먹고, 닫고, 데이터를 다시받을 수 있습니다 . 이 모든 것이 캡슐화되고 MVVM이 승인되었습니다. 사용자 가 창을 닫았을 때와 같은 상태를 얻습니다 . 예를 들어, 사용자 가 창을 Canceled 하거나 Accepted (확인 버튼) 필요 하면 창을 닫고 데이터를 다시 가져옵니다 . 좀 더 복잡하고 대답 # 1,하지만 훨씬 더 완벽하고 Microsoft의 권장 패턴.

필자가 제공 한 링크에는 모든 코드 단편과 예제가 있으므로 여기에 코드를 넣지 않아도됩니다. 프리즘 빠른 시작을 다운로드하여 실행하십시오. 좀 더 자세하게 설명하는 것은 매우 간단합니다. 작동 시키지만 창을 닫는 것보다 더 큰 이점이 있습니다.


Joe White의 대답 과 Adam Mills의 대답 을 혼합하여 결국 프로그래밍 방식으로 생성 된 창에 사용자 정의 컨트롤을 표시해야했습니다. 따라서 DialogCloser는 창에있을 필요가 없으며 사용자 컨트롤 자체에있을 수 있습니다

<UserControl ...
    xmlns:xw="clr-namespace:Wpf"
    xw:DialogCloser.DialogResult="{Binding DialogResult}">

그리고 DialogCloser는 윈도우 자체에 첨부되지 않은 경우 사용자 정의 컨트롤의 윈도우를 찾습니다.

namespace Wpf
{
  public static class DialogCloser
  {
    public static readonly DependencyProperty DialogResultProperty =
        DependencyProperty.RegisterAttached(
            "DialogResult",
            typeof(bool?),
            typeof(DialogCloser),
            new PropertyMetadata(DialogResultChanged));

    private static void DialogResultChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
      var window = d.GetWindow();
      if (window != null)
        window.DialogResult = e.NewValue as bool?;
    }

    public static void SetDialogResult(DependencyObject target, bool? value)
    {
      target.SetValue(DialogResultProperty, value);
    }
  }

  public static class Extensions
  {
    public static Window GetWindow(this DependencyObject sender_)
    {
      Window window = sender_ as Window;        
      return window ?? Window.GetWindow( sender_ );
    }
  }
}

창을 닫을 필요가있는 곳에서는 viewmodel에 다음과 같이 입력하면됩니다.

타다

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }

행동이 여기에서 가장 편리한 방법입니다.

  • 한 손에서, 그것은 주어진 viewmodel에 바인드 될 수 있습니다 ( "close the form!"신호 할 수 있음)

  • 다른 한편으로는 양식 자체에 액세스 할 수 있으므로 필요한 양식 특정 이벤트에 가입하거나 확인 대화 상자를 표시 할 수 있습니다.

필요한 행동을 아주 처음으로 지루하게 볼 수 있습니다. 그러나 이제부터는 정식 one-liner XAML 스 니펫으로 필요한 모든 단일 형식에 다시 사용할 수 있습니다. 필요한 경우 다음 별도의 어셈블리로 추출하여 원하는 다음 프로젝트에 포함 할 수 있습니다.


Application.Current.MainWindow.Close() 

충분 해!


public partial class MyWindow: Window
{
    public ApplicationSelection()
    {
      InitializeComponent();

      MyViewModel viewModel = new MyViewModel();

      DataContext = viewModel;

      viewModel.RequestClose += () => { Close(); };

    }
}

public class MyViewModel
{

  //...Your code...

  public event Action RequestClose;

  public virtual void Close()
  {
    if (RequestClose != null)
    {
      RequestClose();
    }
  }

  public void SomeFunction()
  {
     //...Do something...
     Close();
  }
}




mvvm