ดีหรือไม่ดีสำหรับ Dialogs ใน wpf ด้วย MVVM?


148

เมื่อเร็ว ๆ นี้ฉันมีปัญหาในการสร้างกล่องโต้ตอบเพิ่มและแก้ไขสำหรับแอป wpf ของฉัน

สิ่งที่ฉันต้องการทำในรหัสของฉันคือสิ่งนี้ (ส่วนใหญ่ฉันใช้วิธีแรกใน viewmodel กับ mvvm)

ViewModel ซึ่งเรียกหน้าต่างโต้ตอบ:

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

มันทำงานยังไง?

ก่อนอื่นฉันสร้างบริการโต้ตอบ:

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

WindowDialogเป็นหน้าต่างพิเศษ แต่เรียบง่าย ฉันต้องการมันเพื่อเก็บเนื้อหาของฉัน:

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

ปัญหาเกี่ยวกับการโต้ตอบใน wpf dialogresult = trueสามารถทำได้ในรหัสเท่านั้น นั่นเป็นเหตุผลที่ฉันสร้างอินเทอร์เฟซสำหรับของฉันที่dialogviewmodelจะใช้มัน

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

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

เมื่อใดก็ตามที่ ViewModel ของฉันคิดว่าถึงเวลาdialogresult = trueแล้วให้เพิ่มกิจกรรมนี้

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

อย่างน้อยตอนนี้ฉันต้องสร้างDataTemplateไฟล์ทรัพยากร ( app.xamlหรือบางอย่าง):

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

นั่นคือทั้งหมดที่ฉันสามารถโทรโต้ตอบจากมุมมองของฉัน:

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

ตอนนี้คำถามของฉันคุณเห็นปัญหาใด ๆ กับการแก้ปัญหานี้?

แก้ไข: เพื่อความสมบูรณ์ ViewModel ควรนำไปใช้IDialogResultVMHelperและจากนั้นมันสามารถยกระดับภายในOkCommandหรือสิ่งเช่นนี้:

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

แก้ไข 2: ฉันใช้รหัสจากที่นี่เพื่อทำให้การลงทะเบียน EventHandler ของฉันอ่อนแอ:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(ไม่มีเว็บไซต์แล้วกระจก WebArchive )

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
คุณอาจหายไป xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " การอ้างอิงใน WindowDialog XAML ของคุณ
Adiel Yaacov

อันที่จริงแล้ว namespace คือ xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" โดยไม่มีเครื่องหมายวงเล็บ
reggaeguitar


1
Hi! ผู้มาที่นี่ ฉันไม่เข้าใจว่าบริการของคุณมีการอ้างอิงถึง WindowDialog อย่างไร ลำดับชั้นของแบบจำลองของคุณคืออะไร? ในใจของฉันมุมมองมีการอ้างอิงถึงแอสเซมบลี Viewmodel และ Viewmodel กับแอสเซมบลีของบริการและรูปแบบ ดังนั้นเลเยอร์บริการจะไม่มีความรู้เกี่ยวกับมุมมอง WindowDialog ฉันกำลังคิดถึงอะไร
Moe45673

2
สวัสดี @blindmeis เพียงพยายามคลุมหัวแนวคิดนี้ฉันไม่คิดว่าจะมีตัวอย่างโครงการออนไลน์ที่ฉันสามารถเลือกได้? มีหลายสิ่งที่ฉันสับสน
แฮงค์

คำตอบ:


48

นี่เป็นวิธีการที่ดีและฉันเคยใช้วิธีที่คล้ายกันในอดีต ไปเลย!

สิ่งเล็ก ๆ น้อย ๆ อย่างหนึ่งที่ฉันต้องทำคือทำให้เหตุการณ์ได้รับบูลีนเมื่อคุณจำเป็นต้องตั้งค่า "เท็จ" ใน DialogResult

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

และคลาส EventArgs:

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

    public bool DialogResult { get; private set; }
}

จะเกิดอะไรขึ้นถ้าแทนที่จะใช้บริการหนึ่งจะใช้การเรียกกลับเพื่อจัดเรียงการโต้ตอบกับ ViewModel และมุมมอง ตัวอย่างเช่นมุมมองดำเนินการคำสั่งใน ViewModel จากนั้นเมื่อมีการพูดและดำเนินการทั้งหมด ViewModel จะเรียกการเรียกกลับสำหรับการดูเพื่อแสดงผลลัพธ์ของคำสั่ง ฉันยังไม่สามารถนำทีมของฉันมาที่การใช้บริการเพื่อจัดการกับการโต้ตอบในกล่องโต้ตอบได้
Matthew S

15

ฉันใช้วิธีที่เกือบเหมือนกันมาหลายเดือนแล้วและฉันมีความสุขมากกับมัน (เช่นฉันยังไม่รู้สึกอยากจะเขียนใหม่ทั้งหมด ... )

ในการนำไปใช้ของฉันฉันใช้สิ่งIDialogViewModelที่แสดงสิ่งต่าง ๆ เช่นชื่อปุ่ม standad ที่จะแสดง (เพื่อให้มีความชัดเจนที่สอดคล้องกันในทุกบทสนทนา) RequestCloseเหตุการณ์และอีกสองสามสิ่งเพื่อให้สามารถควบคุมขนาดหน้าต่างและ พฤติกรรม


ขอบคุณชื่อควรไปใน IDialogViewModel ของฉัน คุณสมบัติอื่น ๆ เช่นขนาด, ปุ่มมาตรฐานที่ฉันจะออกไปเพราะทั้งหมดนี้มาจากแผ่นข้อมูลอย่างน้อย
blindmeis

1
นั่นคือสิ่งที่ฉันทำในตอนแรกเช่นกันเพียงใช้ SizeToContent เพื่อควบคุมขนาดของหน้าต่าง แต่ในกรณีหนึ่งที่ฉันจำเป็นเพื่อให้การปรับขนาดหน้าต่างเพื่อให้ฉันได้ปรับแต่งเล็ก ๆ น้อย ๆ ...
โทมัส Levesque

@ThomasLevesque ปุ่มต่างๆที่มีอยู่ใน ViewModel ของคุณจริง ๆ แล้วพวก UI วัตถุหรือปุ่มที่เป็นตัวแทนของปุ่ม?
โทมัส

3
@Thomas วัตถุแทนปุ่ม คุณไม่ควรอ้างอิงวัตถุ UI ใน ViewModel
Thomas Levesque

2

หากคุณกำลังพูดถึงหน้าต่างสนทนาและไม่ใช่เพียงแค่กล่องข้อความแบบผุดขึ้นโปรดพิจารณาแนวทางของฉันด้านล่าง ประเด็นสำคัญคือ:

  1. ฉันส่งการอ้างอิงไปยังModule Controllerผู้สร้างของแต่ละคนViewModel (คุณสามารถใช้การฉีด)
  2. ที่Module Controllerมีวิธีการสาธารณะ / ภายในสำหรับการสร้างหน้าต่างการสนทนา (เพียงแค่สร้างโดยไม่ส่งกลับผลลัพธ์) ดังนั้นเพื่อเปิดหน้าต่างสนทนาในViewModelฉันเขียน:controller.OpenDialogEntity(bla, bla...)
  3. แต่ละแจ้งหน้าต่างการสนทนาเกี่ยวกับผลของมัน (เช่นตกลง , บันทึก , ยกเลิกฯลฯ ) ผ่านกิจกรรมที่อ่อนแอ หากคุณใช้ PRISM คุณสามารถเผยแพร่การแจ้งเตือนโดยใช้EventAggregator นี้ได้ง่ายขึ้นขึ้น
  4. เพื่อจัดการผลลัพธ์ของการสนทนาฉันใช้การสมัครรับการแจ้งเตือน ( เหตุการณ์ที่อ่อนแอและEventAggregatorอีกครั้งในกรณีของ PRISM) เพื่อลดการพึ่งพาการแจ้งเตือนดังกล่าวให้ใช้คลาสอิสระพร้อมการแจ้งเตือนมาตรฐาน

ข้อดี:

  • รหัสน้อย ฉันไม่สนใจการใช้อินเทอร์เฟซ แต่ฉันเห็นโครงการมากเกินไปที่ใช้อินเทอร์เฟซและเลเยอร์นามธรรมมากเกินไปทำให้เกิดปัญหามากกว่าความช่วยเหลือ
  • เปิดหน้าต่างสนทนาผ่าน Module Controllerเป็นวิธีที่ง่ายในการหลีกเลี่ยงการอ้างอิงที่แข็งแกร่งและยังอนุญาตให้ใช้จำลองการทดสอบ
  • การแจ้งเตือนผ่านเหตุการณ์ที่อ่อนแอช่วยลดจำนวนหน่วยความจำรั่ว

จุดด้อย:

  • ไม่ใช่เรื่องง่ายที่จะแยกแยะการแจ้งเตือนที่จำเป็นจากผู้อื่นในตัวจัดการ สองวิธี:
    • ส่งโทเค็นที่ไม่ซ้ำกันในการเปิดหน้าต่างสนทนาและตรวจสอบโทเค็นนั้นในการสมัครสมาชิก
    • ใช้คลาสการแจ้งเตือนทั่วไป<T>ที่Tมีการระบุเอนทิตี (หรือเพื่อความง่ายสามารถเป็นประเภทของ ViewModel)
  • สำหรับโครงการควรเป็นข้อตกลงเกี่ยวกับการใช้คลาสการแจ้งเตือนเพื่อป้องกันการทำซ้ำ
  • สำหรับโครงการที่มีขนาดใหญ่มากModule Controllerสามารถถูกครอบงำด้วยวิธีการสร้างหน้าต่าง ในกรณีนี้จะเป็นการดีกว่าที่จะแยกมันออกเป็นหลายโมดูล

ป.ล. ฉันใช้วิธีนี้มานานแล้วและพร้อมที่จะปกป้องสิทธิ์ในการแสดงความคิดเห็นและให้ตัวอย่างบางอย่างถ้าจำเป็น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.