ViewModel nên đóng biểu mẫu như thế nào?


247

Tôi đang cố gắng tìm hiểu WPF và vấn đề MVVM, nhưng đã gặp phải một trở ngại. Câu hỏi này tương tự nhưng không hoàn toàn giống với câu hỏi này (xử lý các hộp thoại-in-wpf-with-mvvm) ...

Tôi có một biểu mẫu "Đăng nhập" được viết bằng mẫu MVVM.

Biểu mẫu này có ViewModel chứa Tên người dùng và Mật khẩu, được liên kết với chế độ xem trong XAML bằng các ràng buộc dữ liệu thông thường. Nó cũng có lệnh "Đăng nhập" được liên kết với nút "Đăng nhập" trên biểu mẫu, bằng cách sử dụng cơ sở dữ liệu thông thường.

Khi lệnh "Đăng nhập" kích hoạt, nó sẽ gọi một chức năng trong ViewModel sẽ tắt và gửi dữ liệu qua mạng để đăng nhập. Khi chức năng này hoàn thành, có 2 hành động:

  1. Đăng nhập không hợp lệ - chúng tôi chỉ hiển thị MessageBox và tất cả đều ổn

  2. Đăng nhập là hợp lệ, chúng tôi cần phải đóng biểu mẫu Đăng nhập và để nó trở lại đúng như DialogResult...

Vấn đề là, ViewModel không biết gì về chế độ xem thực tế, vậy làm thế nào nó có thể đóng chế độ xem và bảo nó trả về một DialogResult cụ thể ?? Tôi có thể dán một số mã trong CodeBehind và / hoặc chuyển View qua cho ViewModel, nhưng có vẻ như nó sẽ đánh bại toàn bộ điểm của MVVM ...


Cập nhật

Cuối cùng, tôi đã vi phạm "độ tinh khiết" của mẫu MVVM và View đã xuất bản một Closedsự kiện và đưa ra một Closephương thức. ViewModel sau đó sẽ chỉ gọi view.Close. Chế độ xem chỉ được biết qua giao diện và được nối dây qua bộ chứa IOC, do đó không mất khả năng kiểm tra hoặc bảo trì.

Có vẻ khá ngớ ngẩn khi câu trả lời được chấp nhận là ở mức -5 phiếu! Mặc dù tôi nhận thức rõ về những cảm xúc tốt mà người ta có được khi giải quyết vấn đề trong khi "trong sạch", chắc chắn tôi không phải là người duy nhất nghĩ rằng 200 dòng sự kiện, mệnh lệnh và hành vi chỉ để tránh phương pháp một dòng trong tên của "mẫu" và "độ tinh khiết" là một chút vô lý ....


2
Tôi đã không đánh giá thấp câu trả lời được chấp nhận, nhưng tôi đoán lý do cho các downvote là nó không hữu ích nói chung, ngay cả khi nó có thể hoạt động trong một trường hợp. Bạn đã tự nói điều đó trong một bình luận khác: "Mặc dù hình thức đăng nhập là hộp thoại 'hai trường', tôi có nhiều cái khác phức tạp hơn nhiều (và do đó đảm bảo MVVM), nhưng vẫn cần phải đóng ..."
Joe Trắng

1
Tôi thấy quan điểm của bạn, nhưng cá nhân tôi nghĩ rằng ngay cả trong trường hợp chung, một Closephương pháp đơn giản vẫn là giải pháp tốt nhất. Mọi thứ khác trên các hộp thoại phức tạp khác là MVVM và cơ sở dữ liệu, nhưng dường như thật ngớ ngẩn khi thực hiện các "giải pháp" khổng lồ ở đây thay vì chỉ là một phương thức đơn giản ...
Orion Edwards

2
Bạn có thể kiểm tra liên kết sau để biết kết quả hộp thoại asimsajjad.blogspot.com/2010/10/ , sẽ trả lại hộp thoại nối lại và đóng chế độ xem từ viewModel
Asim Sajjad

3
Hãy thay đổi câu trả lời được chấp nhận cho câu hỏi này. Có rất nhiều giải pháp tốt tốt hơn nhiều so với việc ai đó đặt câu hỏi về việc sử dụng MVVM cho chức năng này. Đó không phải là một câu trả lời, đó là sự tránh né.
ScottCher

2
@OrionEdwards Tôi nghĩ bạn đã đúng khi phá vỡ mô hình ở đây. Mục đích chính của mẫu thiết kế là tăng tốc chu kỳ phát triển, tăng khả năng bảo trì và đơn giản hóa mã của bạn bằng cách làm cho cả nhóm tuân theo các quy tắc tương tự. Điều này không đạt được bằng cách thêm các phụ thuộc vào các thư viện bên ngoài và thực hiện hàng trăm dòng mã để hoàn thành một nhiệm vụ, hoàn toàn bỏ qua rằng có một giải pháp đơn giản hơn nhiều, chỉ vì một người ngoan cố hy sinh "độ tinh khiết" của mẫu. Chỉ cần đảm bảo tài liệu gì your've thực hiện và KISS mã của bạn ( k eep i t s Hort và s imple).
M463

Câu trả lời:


324

Tôi đã được truyền cảm hứng từ câu trả lời của Thejuan để viết một tài sản gắn liền đơn giản hơn. Không có phong cách, không có kích hoạt; thay vào đó, bạn chỉ có thể làm điều này:

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

Điều này gần như sạch sẽ như thể nhóm WPF đã hiểu đúng và biến DialogResult thành một tài sản phụ thuộc ngay từ đầu. Chỉ cần đặt mộtbool? DialogResult tính trên ViewModel của bạn và triển khai INotifyPropertyChanged và voilà, ViewModel của bạn có thể đóng Cửa sổ (và đặt DialogResult) chỉ bằng cách đặt thuộc tính. MVVM như nó phải vậy.

Đây là mã cho 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);
        }
    }
}

Tôi cũng đã đăng cái này lên blog của tôi .


3
Đây là câu trả lời tôi thích nhất! Công việc tốt mà viết tài sản kèm theo.
Jorge Vargas

2
Tùy chọn đẹp, nhưng có một lỗi tinh tế trong giải pháp này. Nếu Mô hình xem cho hộp thoại là một đơn, giá trị DialogResult được chuyển sang sử dụng tiếp theo của hộp thoại. Điều đó có nghĩa là nó sẽ ngay lập tức hủy hoặc chấp nhận trước khi hiển thị để hộp thoại sẽ không hiển thị lần thứ hai.
Mã hóa

13
@HiTech Magic, âm thanh giống như lỗi đang sử dụng một ViewModel đơn lẻ ở vị trí đầu tiên. (cười) Nghiêm túc mà nói, tại sao bạn lại muốn có một ViewModel đơn lẻ? Đó là một ý tưởng tồi để giữ trạng thái đột biến trong các biến toàn cầu. Làm cho việc kiểm tra một cơn ác mộng và kiểm tra là một trong những lý do bạn sẽ sử dụng MVVM ngay từ đầu.
Joe White

3
Không phải điểm của MVVM là không kết hợp chặt chẽ logic của bạn với bất kỳ giao diện người dùng cụ thể nào? Trong trường hợp này, bool? chắc chắn là không thể sử dụng được bởi một UI khác như WinForm và DialogCloser dành riêng cho WPF. Vì vậy, làm thế nào để điều này phù hợp cũng như một giải pháp? Ngoài ra, tại sao viết mã 2x-10x chỉ để đóng Cửa sổ thông qua Binding?
David Anderson

2
@DavidAnderson, tôi sẽ không thử MVVM với WinForms trong mọi trường hợp; hỗ trợ cơ sở dữ liệu của nó quá yếu và MVVM dựa trên một hệ thống ràng buộc được cân nhắc kỹ lưỡng. Và nó không ở đâu gần mã 2x-10x. Bạn viết mã đó một lần , không phải một lần cho mỗi cửa sổ. Sau đó, đó là ràng buộc một dòng cộng với thuộc tính thông báo, sử dụng cùng một cơ chế bạn đã sử dụng cho mọi thứ khác trong chế độ xem của mình (ví dụ: bạn không cần phải tiêm giao diện xem thêm chỉ để xử lý việc đóng cửa sổ). Bạn được hoan nghênh thực hiện các sự đánh đổi khác, nhưng có vẻ như đó là một thỏa thuận tốt với tôi.
Joe White

64

Từ quan điểm của tôi, câu hỏi khá hay vì cách tiếp cận tương tự sẽ được sử dụng không chỉ cho cửa sổ "Đăng nhập", mà cho bất kỳ loại cửa sổ nào. Tôi đã xem xét rất nhiều đề xuất và không có gợi ý nào cho tôi. Vui lòng xem lại đề xuất của tôi được lấy từ bài viết mẫu thiết kế MVVM .

Mỗi lớp ViewModel nên kế thừa từ WorkspaceViewModelđó có RequestClosesự kiện và CloseCommandthuộc tính của ICommandloại. Việc thực hiện mặc định của CloseCommandtài sản sẽ nâng cao RequestClosesự kiện.

Để đóng cửa sổ, OnLoadedphương thức của cửa sổ của bạn phải được ghi đè:

void CustomerWindow_Loaded(object sender, RoutedEventArgs e)
{
    CustomerViewModel customer = CustomerViewModel.GetYourCustomer();
    DataContext = customer;
    customer.RequestClose += () => { Close(); };
}

hoặc OnStartupphương pháp của ứng dụng bạn:

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        MainWindow window = new MainWindow();
        var viewModel = new MainWindowViewModel();
        viewModel.RequestClose += window.Close;
        window.DataContext = viewModel;

        window.Show();
    }

Tôi đoán rằng việc triển khai tài sản RequestClosevà sự kiện CloseCommandtrong WorkspaceViewModelkhá rõ ràng, nhưng tôi sẽ cho họ thấy sự nhất quán:

public abstract class WorkspaceViewModel : ViewModelBase
// There's nothing interesting in ViewModelBase as it only implements the INotifyPropertyChanged interface
{
    RelayCommand _closeCommand;
    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
            {
                _closeCommand = new RelayCommand(
                   param => Close(),
                   param => CanClose()
                   );
            }
            return _closeCommand;
        }
    }

    public event Action RequestClose;

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

    public virtual bool CanClose()
    {
        return true;
    }
}

Và mã nguồn của RelayCommand:

public class RelayCommand : ICommand
{
    #region Constructors

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

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

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members

    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields
}

PS Đừng đối xử tệ với tôi vì những nguồn đó! Nếu tôi có chúng ngày hôm qua sẽ giúp tôi tiết kiệm được vài giờ ...

PPS Bất kỳ ý kiến ​​hoặc đề xuất đều được chào đón.


2
Ừm, thực tế bạn đã nối vào trình xử lý sự kiện customer.RequestClosetrong mã phía sau tệp XAML của bạn không vi phạm mẫu MVVM? Bạn cũng có thể liên kết tốt với Clicktrình xử lý sự kiện trên nút đóng của bạn ở vị trí đầu tiên khi thấy bạn đã chạm vào mã phía sau và đã làm a this.Close()! Đúng?
GONeale

1
Tôi không có quá nhiều vấn đề với cách tiếp cận sự kiện nhưng tôi không thích từ RequestClose vì với tôi nó vẫn bao hàm nhiều kiến ​​thức về việc triển khai Chế độ xem. Tôi thích phơi bày các thuộc tính như IsCancelling có xu hướng có ý nghĩa hơn với bối cảnh và ngụ ý ít hơn về những gì quan điểm được cho là phải làm để đáp lại.
jpierson

18

Tôi đã sử dụng các hành vi kèm theo để đóng cửa sổ. Liên kết thuộc tính "tín hiệu" trên ViewModel của bạn với hành vi được đính kèm (Tôi thực sự sử dụng trình kích hoạt) Khi được đặt thành đúng, hành vi sẽ đóng cửa sổ.

http://adammills.wordpress.com/2009/07/01/window-close-from-xaml/


Đây là câu trả lời duy nhất cho đến nay không yêu cầu bất kỳ cơ sở mã hóa nào trong Cửa sổ (và thực sự đóng một Cửa sổ phương thức, thay vì đề xuất một cách tiếp cận khác). Đáng tiếc, nó đòi hỏi rất nhiều sự phức tạp, với Phong cách và Kích hoạt và tất cả những thứ lặt vặt đó - có vẻ như điều này thực sự có thể thực hiện được với hành vi gắn liền một dòng.
Joe White

4
Bây giờ nó có thể thực hiện được với một hành vi đính kèm một dòng. Xem câu trả lời của tôi: stackoverflow.com/questions/501886/ Kẻ
Joe White

15

Có rất nhiều ý kiến ​​tranh luận về ưu và nhược điểm của MVVM tại đây. Đối với tôi, tôi đồng ý với Nir; đó là vấn đề sử dụng mẫu phù hợp và MVVM không phải lúc nào cũng phù hợp. Mọi người dường như đã sẵn sàng hy sinh tất cả các nguyên tắc quan trọng nhất của thiết kế phần mềm CHỈ để làm cho nó phù hợp với MVVM.

Điều đó nói rằng, tôi nghĩ rằng trường hợp của bạn có thể phù hợp với một chút tái cấu trúc.

Trong hầu hết các trường hợp tôi đã gặp, WPF cho phép bạn nhận được bằng cách KHÔNG CÓ nhiều Windows. Có lẽ bạn có thể thử sử dụng Frames và Pages thay vì Windows với DialogResults.

Trong trường hợp của bạn, đề xuất của tôi sẽ LoginFormViewModelxử lý LoginCommandvà nếu đăng nhập không hợp lệ, hãy đặt thuộc tính LoginFormViewModelthành một giá trị thích hợp ( falsehoặc một số giá trị enum như UserAuthenticationStates.FailedAuthentication). Bạn sẽ làm tương tự để đăng nhập thành công ( truehoặc một số giá trị enum khác). Sau đó, bạn sẽ sử dụng DataTriggertrạng thái phản hồi các trạng thái xác thực người dùng khác nhau và có thể sử dụng đơn giản Setterđể thay đổi thuộc Sourcetính của Frame.

Có cửa sổ đăng nhập của bạn trả lại một DialogResulttôi nghĩ là nơi bạn đang bối rối; đó DialogResultthực sự là một tài sản của ViewModel của bạn. Theo tôi, trải nghiệm hạn chế được thừa nhận với WPF, khi điều gì đó không cảm thấy đúng thường là vì tôi đang nghĩ về cách tôi sẽ làm điều tương tự trong WinForms.

Mong rằng sẽ giúp.


10

Giả sử hộp thoại đăng nhập của bạn là cửa sổ đầu tiên được tạo, hãy thử điều này trong lớp LoginViewModel của bạn:

    void OnLoginResponse(bool loginSucceded)
    {
        if (loginSucceded)
        {
            Window1 window = new Window1() { DataContext = new MainWindowViewModel() };
            window.Show();

            App.Current.MainWindow.Close();
            App.Current.MainWindow = window;
        }
        else
        {
            LoginError = true;
        }
    }

Đàn ông này là đơn giản và làm việc tuyệt vời. Hiện tại tôi đang sử dụng phương pháp này.
Erre Efe

Nó chỉ hoạt động cho cửa sổ CHÍNH. Vì vậy, không sử dụng nó cho bất kỳ cửa sổ khác.
Oleksii

7

Đây là một giải pháp đơn giản và gọn gàng - Bạn thêm một sự kiện vào ViewModel và hướng dẫn Cửa sổ tự đóng khi sự kiện đó được kích hoạt.

Để biết thêm chi tiết, xem bài đăng trên blog của tôi, Đóng cửa sổ từ ViewModel .

XAML:

<Window
  x:Name="this"
  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"  
  xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">
  <i:Interaction.Triggers>
    <i:EventTrigger SourceObject="{Binding}" EventName="Closed">
      <ei:CallMethodAction
        TargetObject="{Binding ElementName=this}"
        MethodName="Close"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
<Window>

ViewModel:

private ICommand _SaveAndCloseCommand;
public ICommand SaveAndCloseCommand
{
  get
  {
    return _SaveAndCloseCommand ??
      (_SaveAndCloseCommand = new DelegateCommand(SaveAndClose));
  }
}
private void SaveAndClose()
{
  Save();
  Close();
}

public event EventHandler Closed;
private void Close()
{
  if (Closed != null) Closed(this, EventArgs.Empty);
}

Lưu ý: Ví dụ sử dụng Prism's DelegateCommand(xem Prism: Commanding ), nhưng bất kỳ ICommandtriển khai nào cũng có thể được sử dụng cho vấn đề đó.

Bạn có thể sử dụng các hành vi từ gói chính thức này .


2
+1 nhưng Bạn nên cung cấp thêm chi tiết trong chính câu trả lời, ví dụ: giải pháp này yêu cầu tham chiếu đến cụm tương tác Expression Blend.
lướt

6

Cách tôi sẽ xử lý nó là thêm một trình xử lý sự kiện trong ViewModel của tôi. Khi người dùng đã đăng nhập thành công, tôi sẽ kích hoạt sự kiện. Trong Chế độ xem của tôi, tôi sẽ đính kèm vào sự kiện này và khi nó khởi động, tôi sẽ đóng cửa sổ.


2
Đó cũng là những gì tôi thường làm. Mặc dù điều đó có vẻ hơi bẩn khi xem xét tất cả những thứ chỉ huy wpf mới lạ.
Botz3000

4

Đây là những gì tôi đã làm ban đầu, nó hoạt động, tuy nhiên nó có vẻ khá dài và xấu (mọi thứ tĩnh toàn cầu không bao giờ tốt)

1: Ứng dụng.xaml.cs

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

2: Đăng nhậpForm.xaml

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

3: Đăng nhậpForm.xaml.cs

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

4: Đăng nhậpFormViewModel.cs

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

Sau đó tôi đã xóa tất cả mã này và chỉ có lệnh LoginFormViewModelgọi phương thức Đóng trên chế độ xem. Nó đã trở nên đẹp hơn và dễ theo dõi hơn. IMHO quan điểm của các mẫu là cung cấp cho mọi người một cách dễ dàng hơn để hiểu ứng dụng của bạn đang làm gì và trong trường hợp này, MVVM đã làm cho nó trở nên khó hiểu hơn nhiều so với việc tôi đã không sử dụng nó, và bây giờ là một mô hình chống .


3

FYI, tôi gặp vấn đề tương tự và tôi nghĩ rằng tôi đã tìm ra một công việc xung quanh không yêu cầu toàn cầu hoặc thống kê, mặc dù nó có thể không phải là câu trả lời tốt nhất. Tôi để các bạn quyết định điều đó cho chính mình.

Trong trường hợp của tôi, ViewModel khởi tạo Cửa sổ sẽ được hiển thị (hãy gọi nó là ViewModelMain) cũng biết về LoginFormViewModel (sử dụng tình huống ở trên làm ví dụ).

Vì vậy, những gì tôi đã làm là tạo một thuộc tính trên LoginFormViewModel thuộc loại ICommand (Hãy gọi nó là CloseWindowCommand). Sau đó, trước khi tôi gọi .ShowDialog () trên Cửa sổ, tôi đã đặt thuộc tính CloseWindowCommand trên phương thức LoginFormViewModel cho cửa sổ. Sau đó, bên trong LoginFormViewModel, tất cả những gì tôi phải làm là gọi CloseWindowCommand.Execute () để đóng cửa sổ.

Tôi cho rằng đó là một cách giải quyết / hack, nhưng nó hoạt động tốt mà không thực sự phá vỡ mô hình MVVM.

Hãy phê bình quá trình này nhiều như bạn muốn, tôi có thể thực hiện nó! :)


Tôi không chắc chắn tôi đã hoàn toàn mò mẫm nó, nhưng điều này không có nghĩa là MainWindow của bạn phải được khởi tạo trước Đăng nhập của bạn? Đó là điều tôi muốn tránh nếu có thể
Orion Edwards

3

Điều này có lẽ rất muộn, nhưng tôi đã gặp phải vấn đề tương tự và tôi đã tìm ra giải pháp hiệu quả cho mình.

Tôi không thể tìm ra cách tạo một ứng dụng mà không có hộp thoại (có thể đó chỉ là một khối tâm trí). Vì vậy, tôi đã ở trong tình trạng bế tắc với MVVM và hiển thị một hộp thoại. Vì vậy, tôi đã xem qua bài viết CodeProject này:

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

Đó là UserControl về cơ bản cho phép một cửa sổ nằm trong cây trực quan của một cửa sổ khác (không được phép trong xaml). Nó cũng hiển thị một DependencyProperty boolean được gọi là IsShowing.

Bạn có thể đặt kiểu như, điển hình là trong một nguồn tài nguyên có nguồn gốc, về cơ bản sẽ hiển thị hộp thoại bất cứ khi nào thuộc tính Nội dung của điều khiển! = Null thông qua trình kích hoạt:

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

Trong chế độ xem bạn muốn hiển thị hộp thoại, chỉ cần có:

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

Và trong ViewModel của bạn, tất cả những gì bạn phải làm là đặt thuộc tính thành một giá trị (Lưu ý: lớp ViewModel phải hỗ trợ INotifyPropertyChanged để xem để biết điều gì đó đã xảy ra).

như vậy

DialogViewModel = new DisplayViewModel();

Để khớp với ViewModel với Chế độ xem, bạn nên có một cái gì đó như thế này trong một nguồn từ khóa:

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

Với tất cả điều đó, bạn nhận được một mã một lớp để hiển thị hộp thoại. Vấn đề bạn nhận được là bạn không thể thực sự đóng hộp thoại chỉ với đoạn mã trên. Vì vậy, đó là lý do tại sao bạn phải đưa một sự kiện vào lớp cơ sở ViewModel mà DisplayViewModel kế thừa từ và thay vì mã ở trên, hãy viết điều này

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

Sau đó, bạn có thể xử lý kết quả của hộp thoại thông qua cuộc gọi lại.

Điều này có vẻ hơi phức tạp, nhưng một khi nền tảng được đặt ra, nó khá đơn giản. Một lần nữa đây là triển khai của tôi, tôi chắc chắn có những người khác :)

Hy vọng điều này sẽ giúp, nó đã cứu tôi.


3

Ok, vì vậy câu hỏi này đã gần 6 tuổi và tôi vẫn không thể tìm thấy ở đây điều mà tôi nghĩ đó là câu trả lời thích hợp, vì vậy hãy cho phép tôi chia sẻ "2 xu" của mình ...

Tôi thực sự có 2 cách để thực hiện, cách thứ nhất là cách đơn giản ... cách thứ hai ở bên phải, vì vậy nếu bạn đang tìm đúng cách, chỉ cần bỏ qua # 1 và nhảy đến # 2 :

1. Nhanh chóng và dễ dàng (nhưng không đầy đủ)

Nếu tôi chỉ có một dự án nhỏ, đôi khi tôi chỉ cần tạo một CloseWindowAction trong ViewModel:

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

Và bất cứ ai đặt Chế độ xem hoặc trong mã của Chế độ xem phía sau tôi chỉ cần đặt Phương thức mà Hành động sẽ gọi:

.

Nếu một số ViewModel tạo một cửa sổ mới:

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

Hoặc nếu bạn muốn nó trong Cửa sổ chính của bạn, chỉ cần đặt nó bên dưới hàm tạo của View:

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

khi bạn muốn đóng cửa sổ, chỉ cần gọi Hành động trên ViewModel của bạn.


2. Đúng cách

Bây giờ cách làm đúng là sử dụng Prism (IMHO), và tất cả về nó có thể được tìm thấy ở đây .

Bạn có thể thực hiện Yêu cầu tương tác , điền vào bất kỳ dữ liệu nào bạn cần trong Cửa sổ mới, ăn trưa, đóng và thậm chí nhận lại dữ liệu . Tất cả điều này được đóng gói và MVVM được phê duyệt. Bạn thậm chí còn nhận được trạng thái về cách Cửa sổ được đóng , như nếu Người dùng CanceledhoặcAccepted (nút OK) Cửa sổ và dữ liệu trở lại nếu bạn cần . Nó phức tạp hơn một chút và Câu trả lời số 1, nhưng nó hoàn chỉnh hơn rất nhiều và Mẫu được đề xuất bởi Microsoft.

Liên kết tôi đưa ra có tất cả các đoạn mã và ví dụ, vì vậy tôi sẽ không bận tâm đặt bất kỳ mã nào vào đây, chỉ cần đọc bài viết tải xuống Prism Quick Start và chạy nó, thật đơn giản để hiểu rõ hơn một chút làm cho nó hoạt động, nhưng lợi ích lớn hơn là chỉ đóng một cửa sổ.


Cách hay, nhưng độ phân giải và sự phân công của ViewModels không thể luôn luôn như vậy. Điều gì xảy ra nếu cùng một viewmodel là DataContext của nhiều Windows?
Kylo Ren

Sau đó, tôi đoán bạn phải đóng tất cả các cửa sổ cùng một lúc, hãy nhớ rằng một Hành động có thể kích hoạt nhiều đại biểu cùng một lúc, chỉ cần sử dụng +=để thêm một đại biểu và gọi Hành động, nó sẽ kích hoạt tất cả chúng .... Hoặc bạn sẽ phải tạo một logic đặc biệt trên máy ảo của bạn để nó biết được cửa sổ nào sẽ đóng (có thể có một bộ Hành động Đóng) .... Nhưng tôi nghĩ rằng việc có nhiều khung nhìn liên kết với một VM không phải là cách tốt nhất, đó là tốt hơn để quản lý để có một Chế độ xem và một VM, liên kết với nhau và có thể là một VM cha quản lý tất cả các VM con được liên kết với tất cả các Chế độ xem.
mFeinstein

3
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();
  }
}

2

Bạn có thể để ViewModel hiển thị một sự kiện mà View đăng ký. Sau đó, khi ViewModel quyết định thời gian đóng chế độ xem, nó sẽ kích hoạt sự kiện đó khiến cho chế độ xem đóng. Nếu bạn muốn một giá trị kết quả cụ thể được trả lại, thì bạn sẽ có một thuộc tính trong ViewModel cho điều đó.


Tôi đồng ý với điều này - sự đơn giản là có giá trị. Tôi phải suy nghĩ về những gì xảy ra khi nhà phát triển cơ sở tiếp theo được thuê để tiếp quản dự án này. Tôi đoán là anh ấy sẽ có cơ hội tốt hơn nhiều để có được điều này đúng như bạn mô tả. Trừ khi bạn nghĩ rằng bạn sẽ duy trì mã này mãi mãi? +1
Trưởng khoa

2

Chỉ cần thêm vào số lượng lớn câu trả lời, tôi muốn thêm vào như sau. Giả sử rằng bạn có ICommand trên ViewModel của bạn và bạn muốn lệnh đó đóng cửa sổ của nó (hoặc bất kỳ hành động nào khác cho vấn đề đó), bạn có thể sử dụng một cái gì đó như sau.

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

Nó không hoàn hảo và có thể khó kiểm tra (vì khó có thể giả lập / khai thác tĩnh) nhưng nó sạch hơn (IMHO) so với các giải pháp khác.

Erick


Tôi trở nên rất hạnh phúc khi nhìn thấy câu trả lời đơn giản của bạn! nhưng nó cũng không hoạt động! Tôi cần phải mở và đóng với cơ bản trực quan. Bạn có biết sự tương đương của (windows [i] .DataContext == this) trong VB không?
Ehsan

Tôi đã nhận được nó cuối cùng! :) Cảm ơn. Nếu cửa sổ (i) .DataContext là tôi
Ehsan

Bạn có biết cách đơn giản tương tự để mở một cửa sổ không? Tôi cần gửi và nhận một số dữ liệu trong viewmodel con và ngược lại.
Ehsan

1

Tôi đã triển khai giải pháp của Joe White, nhưng thỉnh thoảng gặp sự cố " DialogResult chỉ có thể được đặt sau khi Window được tạo và hiển thị dưới dạng hộp thoại ".

Tôi đã giữ ViewModel xung quanh sau khi Chế độ xem được đóng và thỉnh thoảng sau đó tôi đã mở Chế độ xem mới bằng cùng một máy ảo. Có vẻ như việc đóng Chế độ xem mới trước khi Chế độ xem cũ bị thu gom rác dẫn đến DialogResultChanged cố gắng đặt DialogResult trên cửa sổ đã đóng, do đó gây ra lỗi.

Giải pháp của tôi là thay đổi DialogResultChanged để kiểm tra thuộc tính IsLoaded của cửa sổ :

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

Sau khi thực hiện thay đổi này, mọi tệp đính kèm vào hộp thoại đã đóng sẽ bị bỏ qua.


Cảm ơn ngài. Tôi đã có cùng một vấn đề
DJ Burb

1

Cuối cùng tôi đã pha trộn câu trả lời của Joe White và một số mã từ câu trả lời của Adam Mills , vì tôi cần hiển thị một điều khiển người dùng trong một cửa sổ được lập trình. Vì vậy, DialogCloser không cần phải ở trên cửa sổ, nó có thể nằm trên chính điều khiển của người dùng

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

Và DialogCloser sẽ tìm thấy cửa sổ điều khiển người dùng nếu nó không được gắn vào chính cửa sổ đó.

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_ );
    }
  }
}

1

Hành vi là cách thuận tiện nhất ở đây.

  • Từ một mặt, nó có thể được liên kết với khung nhìn đã cho (có thể báo hiệu "đóng biểu mẫu!")

  • Từ một mặt khác, nó có quyền truy cập vào biểu mẫu để có thể đăng ký các sự kiện cụ thể của biểu mẫu hoặc hiển thị hộp thoại xác nhận hoặc bất cứ điều gì khác.

Viết hành vi cần thiết có thể được nhìn thấy nhàm chán ngay lần đầu tiên. Tuy nhiên, kể từ bây giờ, bạn có thể sử dụng lại nó trên mọi hình thức bạn cần bằng đoạn trích XAML một lớp chính xác. Và nếu cần thiết, bạn có thể trích xuất nó dưới dạng một hội đồng riêng biệt để có thể đưa nó vào bất kỳ dự án tiếp theo nào bạn muốn.


0

Tại sao không chỉ vượt qua cửa sổ như một tham số lệnh?

C #:

 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}" />

Tôi không nghĩ nên hạn chế VM ở loại Window.
Shimmy Weitzhandler

2
Tôi không nghĩ rằng nên hạn chế VM thành một Windowloại MVVM không "thuần túy". Xem câu trả lời này , trong đó VM không bị giới hạn Windowđối tượng.
Shimmy Weitzhandler

bằng cách này, sự phụ thuộc đang được đặt vào Nút mà chắc chắn không thể luôn luôn là tình huống. Ngoài ra, việc chuyển loại UI cho ViewModel là một thực tiễn tồi.
Kylo Ren

0

Một giải pháp khác là tạo thuộc tính với INotifyPropertyChanged trong Chế độ xem như DialogResult, và sau đó trong Code Phía sau viết điều này:

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;
        }
    }
}

Đoạn quan trọng nhất là _someViewModel_PropertyChanged. DialogResultPropertyNamecó thể là một số chuỗi const công khai trong SomeViewModel.

Tôi sử dụng loại mẹo này để thực hiện một số thay đổi trong Điều khiển xem trong trường hợp khó thực hiện trong ViewModel. OnPropertyChanged trong ViewModel bạn có thể làm bất cứ điều gì bạn muốn trong View. ViewModel vẫn là 'đơn vị có thể kiểm tra' và một số dòng mã nhỏ phía sau mã không có sự khác biệt.


0

Tôi sẽ đi theo con đường này:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;    
using GalaSoft.MvvmLight.Messaging; 

// View

public partial class TestCloseWindow : Window
{
    public TestCloseWindow() {
        InitializeComponent();
        Messenger.Default.Register<CloseWindowMsg>(this, (msg) => Close());
    }
}

// View Model

public class MainViewModel: ViewModelBase
{
    ICommand _closeChildWindowCommand;

    public ICommand CloseChildWindowCommand {
        get {
            return _closeChildWindowCommand?? (_closeChildWindowCommand = new RelayCommand(() => {
                Messenger.Default.Send(new CloseWindowMsg());
        }));
        }
    }
}

public class CloseWindowMsg
{
}

0

Tôi đã đọc tất cả các câu trả lời nhưng tôi phải nói rằng, hầu hết trong số chúng chỉ không đủ tốt hoặc thậm chí tệ hơn.

Bạn có thể xử lý việc này một cách tuyệt vời với lớp DialogService , trách nhiệm là hiển thị cửa sổ hộp thoại và trả về kết quả hộp thoại. Tôi đã tạo dự án mẫu cho thấy việc thực hiện và sử dụng nó.

Đây là những phần quan trọng nhất:

//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();
    }
}

Đây không phải là đơn giản hơn? eo biển hơn, dễ đọc hơn và cuối cùng nhưng không kém phần dễ gỡ lỗi hơn EventAggregator hoặc các giải pháp tương tự khác?

như bạn có thể thấy, trong các mô hình chế độ xem của tôi, tôi đã sử dụng phương pháp tiếp cận đầu tiên của ViewModel được mô tả trong bài đăng của tôi ở đây: Cách tốt nhất để gọi View từ ViewModel trong WPF

Tất nhiên, trong thế giới thực, DialogService.ShowDialogphải có nhiều tùy chọn hơn để định cấu hình hộp thoại, ví dụ: các nút và lệnh họ sẽ thực thi. Có nhiều cách khác nhau để làm như vậy, nhưng nó nằm ngoài phạm vi :)


0

Mặc dù điều này không trả lời câu hỏi về cách thực hiện điều này thông qua chế độ xem, nhưng điều này cho thấy cách thực hiện chỉ bằng XAML + SDK pha trộn.

Tôi đã chọn tải xuống và sử dụng hai tệp từ Blend SDK, cả hai đều có thể là một gói từ Microsoft thông qua NuGet. Các tập tin là:

System.Windows.Interactivity.dll và Microsoft.Expression.Interilities.dll

Microsoft.Expression.Interilities.dll cung cấp cho bạn các khả năng tuyệt vời như khả năng đặt thuộc tính hoặc gọi một phương thức trên chế độ xem của bạn hoặc mục tiêu khác và cũng có các tiện ích khác bên trong.

Một số 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>

Lưu ý rằng nếu bạn chỉ thực hiện hành vi OK / Hủy đơn giản, bạn có thể thoát khỏi việc sử dụng các thuộc tính IsDefault và Is Hủy miễn là cửa sổ được hiển thị w / Window.ShowDialog ().
Cá nhân tôi đã gặp sự cố với một nút có thuộc tính IsDefault được đặt thành đúng, nhưng nó đã bị ẩn khi trang được tải. Nó dường như không muốn chơi độc đáo sau khi nó được hiển thị, vì vậy tôi chỉ thiết lập thuộc tính Window.DialogResult như được hiển thị ở trên và nó hoạt động với tôi.


0

Đây là giải pháp không có lỗi đơn giản (có mã nguồn), Nó hoạt động với tôi.

  1. Xuất phát ViewModel của bạn từ INotifyPropertyChanged

  2. Tạo một thuộc tính có thể quan sát được CloseDialog trong ViewModel

    public void Execute()
    {
        // Do your task here
    
        // if task successful, assign true to CloseDialog
        CloseDialog = true;
    }
    
    private bool _closeDialog;
    public bool CloseDialog
    {
        get { return _closeDialog; }
        set { _closeDialog = value; OnPropertyChanged(); }
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    private void OnPropertyChanged([CallerMemberName]string property = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    }

  3. Đính kèm một Handler trong View để thay đổi thuộc tính này

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Bây giờ bạn đã gần xong. Trong xử lý sự kiệnDialogResult = true

    protected void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == "CloseDialog")
        {
            DialogResult = true;
        }
    }

0

Tạo một Dependency Propertytrong View/ bất kỳ UserControl(hoặc Windowbạn muốn đóng). Giống như dưới đây:

 public bool CloseTrigger
        {
            get { return (bool)GetValue(CloseTriggerProperty); }
            set { SetValue(CloseTriggerProperty, value); }
        }

        public static readonly DependencyProperty CloseTriggerProperty =
            DependencyProperty.Register("CloseTrigger", typeof(bool), typeof(ControlEventBase), new PropertyMetadata(new PropertyChangedCallback(OnCloseTriggerChanged)));

        private static void OnCloseTriggerChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            //write Window Exit Code
        }

Và liên kết nó từ tài sản của ViewModel của bạn :

<Window x:Class="WpfStackOverflowTempProject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"  Width="525"
        CloseTrigger="{Binding Path=CloseWindow,Mode=TwoWay}"

Tài sản trong VeiwModel:

private bool closeWindow;

    public bool CloseWindow
    {
        get { return closeWindow; }
        set 
        { 
            closeWindow = value;
            RaiseChane("CloseWindow");
        }
    }

Bây giờ kích hoạt hoạt động đóng bằng cách thay đổi CloseWindowgiá trị trong ViewModel. :)


-2

Nơi bạn cần đóng cửa sổ, chỉ cần đặt cái này trong viewmodel:

ta-da

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

Một ViewModel không được chứa một UIElement trong bất kỳ cách nào, bởi vì điều này có thể tạo ra lỗi của
WiiMaxx

Điều gì xảy ra nếu DataContext đang được kế thừa một số cửa sổ?
Kylo Ren

ta-da, đây hoàn toàn không phải là MVVM.
Alexandru Dicu

-10
Application.Current.MainWindow.Close() 

Thế là đủ rồi!


3
-1 Chỉ đúng nếu cửa sổ mà bạn muốn đóng là cửa sổ chính ... Giả định rất khó xảy ra cho hộp thoại Đăng nhập ...
lướt
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.