Thực hành tốt hay xấu cho Hộp thoại trong wpf với MVVM?


148

Gần đây tôi gặp vấn đề trong việc tạo các hộp thoại thêm và chỉnh sửa cho ứng dụng wpf của mình.

Tất cả tôi muốn làm trong mã của tôi là một cái gì đó như thế này. (Tôi chủ yếu sử dụng phương pháp tiếp cận đầu tiên của viewmodel với mvvm)

ViewModel gọi cửa sổ hộp thoại:

var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);
// Do anything with the dialog result

Làm thế nào nó hoạt động?

Đầu tiên, tôi tạo một dịch vụ hộp thoại:

public interface IUIWindowDialogService
{
    bool? ShowDialog(string title, object datacontext);
}

public class WpfUIWindowDialogService : IUIWindowDialogService
{
    public bool? ShowDialog(string title, object datacontext)
    {
        var win = new WindowDialog();
        win.Title = title;
        win.DataContext = datacontext;

        return win.ShowDialog();
    }
}

WindowDialoglà một cửa sổ đặc biệt nhưng đơn giản. Tôi cần nó để giữ nội dung của tôi:

<Window x:Class="WindowDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    Title="WindowDialog" 
    WindowStyle="SingleBorderWindow" 
    WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
    <ContentPresenter x:Name="DialogPresenter" Content="{Binding .}">

    </ContentPresenter>
</Window>

Một vấn đề với các hộp thoại trong wpf là dialogresult = truechỉ có thể đạt được trong mã. Đó là lý do tại sao tôi tạo ra một giao diện để tôi dialogviewmodelthực hiện nó.

public class RequestCloseDialogEventArgs : EventArgs
{
    public bool DialogResult { get; set; }
    public RequestCloseDialogEventArgs(bool dialogresult)
    {
        this.DialogResult = dialogresult;
    }
}

public interface IDialogResultVMHelper
{
    event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
}

Bất cứ khi nào ViewModel của tôi nghĩ rằng đã đến lúc dialogresult = true, hãy nâng cao sự kiện này.

public partial class DialogWindow : Window
{
    // Note: If the window is closed, it has no DialogResult
    private bool _isClosed = false;

    public DialogWindow()
    {
        InitializeComponent();
        this.DialogPresenter.DataContextChanged += DialogPresenterDataContextChanged;
        this.Closed += DialogWindowClosed;
    }

    void DialogWindowClosed(object sender, EventArgs e)
    {
        this._isClosed = true;
    }

    private void DialogPresenterDataContextChanged(object sender,
                              DependencyPropertyChangedEventArgs e)
    {
        var d = e.NewValue as IDialogResultVMHelper;

        if (d == null)
            return;

        d.RequestCloseDialog += new EventHandler<RequestCloseDialogEventArgs>
                                    (DialogResultTrueEvent).MakeWeak(
                                        eh => d.RequestCloseDialog -= eh;);
    }

    private void DialogResultTrueEvent(object sender, 
                              RequestCloseDialogEventArgs eventargs)
    {
        // Important: Do not set DialogResult for a closed window
        // GC clears windows anyways and with MakeWeak it
        // closes out with IDialogResultVMHelper
        if(_isClosed) return;

        this.DialogResult = eventargs.DialogResult;
    }
 }

Bây giờ ít nhất tôi phải tạo một DataTemplatetệp trong tài nguyên của mình ( app.xamlhoặc một cái gì đó):

<DataTemplate DataType="{x:Type DialogViewModel:EditOrNewAuswahlItemVM}" >
        <DialogView:EditOrNewAuswahlItem/>
</DataTemplate>

Vâng đó là tất cả, bây giờ tôi có thể gọi các hộp thoại từ chế độ xem của mình:

 var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);

Bây giờ câu hỏi của tôi, bạn có thấy bất kỳ vấn đề với giải pháp này?

Chỉnh sửa: cho đầy đủ. ViewModel nên triển khai IDialogResultVMHelpervà sau đó nó có thể nâng cao nó trong một OkCommandhoặc một cái gì đó như thế này:

public class MyViewmodel : IDialogResultVMHelper
{
    private readonly Lazy<DelegateCommand> _okCommand;

    public MyViewmodel()
    {
         this._okCommand = new Lazy<DelegateCommand>(() => 
             new DelegateCommand(() => 
                 InvokeRequestCloseDialog(
                     new RequestCloseDialogEventArgs(true)), () => 
                         YourConditionsGoesHere = true));
    }

    public ICommand OkCommand
    { 
        get { return this._okCommand.Value; } 
    }

    public event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
    private void InvokeRequestCloseDialog(RequestCloseDialogEventArgs e)
    {
        var handler = RequestCloseDialog;
        if (handler != null) 
            handler(this, e);
    }
 }

EDIT 2: Tôi đã sử dụng mã từ đây để làm cho đăng ký EventHandler của mình yếu đi:
http://diditwith.net/2007/03/23/SolveThePro HiệuWithEventsWeakEventHandlers.aspx
(Trang web không còn tồn tại, WebArchive Mirror )

public delegate void UnregisterCallback<TE>(EventHandler<TE> eventHandler) 
    where TE : EventArgs;

public interface IWeakEventHandler<TE> 
    where TE : EventArgs
{
    EventHandler<TE> Handler { get; }
}

public class WeakEventHandler<T, TE> : IWeakEventHandler<TE> 
    where T : class 
    where TE : EventArgs
{
    private delegate void OpenEventHandler(T @this, object sender, TE e);

    private readonly WeakReference mTargetRef;
    private readonly OpenEventHandler mOpenHandler;
    private readonly EventHandler<TE> mHandler;
    private UnregisterCallback<TE> mUnregister;

    public WeakEventHandler(EventHandler<TE> eventHandler,
                                UnregisterCallback<TE> unregister)
    {
        mTargetRef = new WeakReference(eventHandler.Target);

        mOpenHandler = (OpenEventHandler)Delegate.CreateDelegate(
                           typeof(OpenEventHandler),null, eventHandler.Method);

        mHandler = Invoke;
        mUnregister = unregister;
    }

    public void Invoke(object sender, TE e)
    {
        T target = (T)mTargetRef.Target;

        if (target != null)
            mOpenHandler.Invoke(target, sender, e);
        else if (mUnregister != null)
        {
            mUnregister(mHandler);
            mUnregister = null;
        }
    }

    public EventHandler<TE> Handler
    {
        get { return mHandler; }
    }

    public static implicit operator EventHandler<TE>(WeakEventHandler<T, TE> weh)
    {
        return weh.mHandler;
    }
}

public static class EventHandlerUtils
{
    public static EventHandler<TE> MakeWeak<TE>(this EventHandler<TE> eventHandler, 
                                                    UnregisterCallback<TE> unregister)
        where TE : EventArgs
    {
        if (eventHandler == null)
            throw new ArgumentNullException("eventHandler");

        if (eventHandler.Method.IsStatic || eventHandler.Target == null)
            throw new ArgumentException("Only instance methods are supported.",
                                            "eventHandler");

        var wehType = typeof(WeakEventHandler<,>).MakeGenericType(
                          eventHandler.Method.DeclaringType, typeof(TE));

        var wehConstructor = wehType.GetConstructor(new Type[] 
                             { 
                                 typeof(EventHandler<TE>), typeof(UnregisterCallback<TE>) 
                             });

        IWeakEventHandler<TE> weh = (IWeakEventHandler<TE>)wehConstructor.Invoke(
                                        new object[] { eventHandler, unregister });

        return weh.Handler;
    }
}

1
có lẽ bạn đang thiếu xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " giới thiệu trong WindowDialog XAML của bạn.
Adiel Yaacov

Trên thực tế, không gian tên là xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" không có dấu ngoặc
reggaeg Ức


1
Chào! Latecomer đây. Tôi không hiểu làm thế nào Dịch vụ của bạn có tham chiếu đến WindowDialog. Hệ thống mô hình của bạn là gì? Trong tâm trí của tôi, View giữ một tham chiếu đến cụm Viewmodel và Viewmodel cho các cụm dịch vụ và Model. Do đó, lớp Service sẽ không có kiến ​​thức về khung nhìn WindowDialog. Tôi đang thiếu gì?
Moe45673

2
Xin chào @blindmeis, chỉ cần cố gắng xoay quanh khái niệm này, tôi không cho rằng có một số dự án ví dụ trực tuyến tôi có thể chọn? Có một số điều tôi bối rối.
Hank

Câu trả lời:


48

Đây là một cách tiếp cận tốt và tôi đã sử dụng những cách tương tự trong quá khứ. Cứ liều thử đi!

Một điều nhỏ mà tôi chắc chắn sẽ làm là làm cho sự kiện nhận được boolean khi bạn cần đặt "false" trong DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

và lớp EventArss:

public class RequestCloseEventArgs : EventArgs
{
    public RequestCloseEventArgs(bool dialogResult)
    {
        this.DialogResult = dialogResult;
    }

    public bool DialogResult { get; private set; }
}

Điều gì sẽ xảy ra nếu thay vì sử dụng các dịch vụ, người ta sử dụng một loại Gọi lại để tạo thuận lợi cho việc tương tác với ViewModel và Chế độ xem? Ví dụ: View thực thi một Lệnh trong ViewModel, sau đó khi tất cả được nói và thực hiện, ViewModel sẽ thực hiện Gọi lại cho Chế độ xem để hiển thị kết quả của Lệnh. Tôi vẫn không thể đưa nhóm của mình lên tàu bằng cách sử dụng Dịch vụ để xử lý các tương tác Hộp thoại trong ViewModel.
Matthew S

15

Tôi đã sử dụng một cách tiếp cận gần như giống hệt nhau trong vài tháng nay và tôi rất hài lòng với nó (tức là tôi chưa cảm thấy muốn viết lại hoàn toàn ...)

Trong triển khai của mình, tôi sử dụng một IDialogViewModelhiển thị những thứ như tiêu đề, các nút độc lập để hiển thị (để có sự rõ ràng nhất quán trên tất cả các hộp thoại), một RequestClosesự kiện và một vài thứ khác để có thể kiểm soát kích thước cửa sổ và hành vi


Thx, tiêu đề thực sự nên có trong IDialogViewModel của tôi. các thuộc tính khác như kích thước, nút tiêu chuẩn tôi sẽ để lại, bởi vì tất cả điều này xuất phát từ dữ liệu ít nhất.
blindmeis

1
Đó là những gì tôi đã làm lúc đầu, chỉ cần sử dụng SizeToContent để kiểm soát kích thước của cửa sổ. Nhưng trong một trường hợp tôi cần phải làm cho cửa sổ có thể thay đổi kích thước, vì vậy tôi phải điều chỉnh nó một chút ...
Thomas Levesque

@ThomasLevesque các nút có trong ViewModel của bạn, chúng thực sự là các đối tượng Nút UI hoặc các đối tượng đại diện cho các nút?
Thomas

3
@Thomas, đối tượng đại diện cho các nút. Bạn không bao giờ nên tham chiếu các đối tượng UI trong ViewModel.
Thomas Levesque

2

Nếu bạn đang nói về các cửa sổ đối thoại và không chỉ về các hộp thông báo bật lên, vui lòng xem xét phương pháp của tôi dưới đây. Những điểm chính là:

  1. Tôi chuyển một tham chiếu đến Module Controllerhàm tạo của từng hàm ViewModel(bạn có thể sử dụng phép tiêm).
  2. Điều đó Module Controllercó các phương thức công khai / nội bộ để tạo các cửa sổ đối thoại (chỉ tạo, không trả về kết quả). Do đó để mở một cửa sổ đối thoại trong ViewModeltôi viết:controller.OpenDialogEntity(bla, bla...)
  3. Mỗi cửa sổ hội thoại thông báo về kết quả của nó (như OK , Lưu , Hủy , v.v.) thông qua các Sự kiện Yếu . Nếu bạn sử dụng PRISM, thì việc xuất bản thông báo bằng EventAggregator này sẽ dễ dàng hơn .
  4. Để kết quả đối thoại xử lý, tôi đang sử dụng thuê bao thông báo (một lần nữa sự kiện YếuEventAggregator trong trường hợp PRISM). Để giảm sự phụ thuộc vào các thông báo như vậy, hãy sử dụng các lớp độc lập với thông báo tiêu chuẩn.

Ưu điểm:

  • Ít mã hơn. Tôi không ngại sử dụng các giao diện, nhưng tôi đã thấy quá nhiều dự án trong đó việc sử dụng các giao diện và các lớp trừu tượng quá mức gây ra nhiều rắc rối hơn là giúp đỡ.
  • Mở các cửa sổ đối thoại thông qua Module Controllerlà một cách đơn giản để tránh các tài liệu tham khảo mạnh mẽ và vẫn cho phép sử dụng các mô hình giả để thử nghiệm.
  • Thông báo thông qua các sự kiện yếu làm giảm số lượng rò rỉ bộ nhớ tiềm năng.

Nhược điểm:

  • Không dễ để phân biệt thông báo cần thiết với những người khác trong xử lý. Hai giải pháp:
    • gửi mã thông báo duy nhất khi mở cửa sổ hội thoại và kiểm tra mã thông báo đó trong đăng ký
    • sử dụng các lớp thông báo chung <T>trong đó Tliệt kê các thực thể (hoặc để đơn giản, nó có thể là loại ViewModel).
  • Đối với một dự án nên là một thỏa thuận về việc sử dụng các lớp thông báo để ngăn chặn việc sao chép chúng.
  • Đối với các dự án rất lớn, Module Controllercó thể bị áp đảo bởi các phương pháp tạo cửa sổ. Trong trường hợp này, tốt hơn là chia nó thành nhiều mô-đun.

PS Tôi đã sử dụng phương pháp này khá lâu rồi và sẵn sàng bảo vệ tính đủ điều kiện của nó trong các bình luận và cung cấp một số ví dụ nếu được yêu cầu.

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.