การจัดการเหตุการณ์การปิดหน้าต่างด้วย WPF / MVVM Light Toolkit


145

ฉันต้องการจัดการ Closingกิจกรรม (เมื่อผู้ใช้คลิกปุ่ม 'X' ด้านขวาบน) ของหน้าต่างของฉันเพื่อแสดงข้อความยืนยันหรือ / และยกเลิกการปิดท้าย

ฉันรู้วิธีการทำเช่นนี้ในโค้ด - เบื้องหลัง: สมัครสมาชิกกับClosingเหตุการณ์ของหน้าต่างจากนั้นใช้CancelEventArgs.Cancelคุณสมบัติ

แต่ฉันใช้ MVVM ฉันไม่แน่ใจว่ามันเป็นแนวทางที่ดี

ฉันคิดว่าวิธีการที่ดีคือการผูกClosingกิจกรรมไว้Commandใน ViewModel ของฉัน

ฉันลองแล้ว:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding CloseCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

ด้วยการเชื่อมโยงRelayCommandใน ViewModel ของฉัน แต่มันไม่ทำงาน (รหัสของคำสั่งไม่ได้ถูกเรียกใช้)


3
ยังสนใจในคำตอบที่ดีที่จะตอบคำถามนี้
Sekhat

3
ฉันดาวน์โหลดรหัสจาก codeplex และตรวจแก้จุดบกพร่องพบว่า: "ไม่สามารถโยนวัตถุประเภท 'System.ComponentModel.CancelEventArgs' เพื่อพิมพ์ 'System.Windows.RoutedEventArgs' มันใช้งานได้ดีถ้าคุณไม่ต้องการ CancelEventArgs แต่นั่นไม่ได้ตอบคำถามของคุณ ...
David Hollinshead

ฉันคาดเดารหัสของคุณไม่ทำงานเนื่องจากการควบคุมที่คุณแนบทริกเกอร์เพื่อไม่มีเหตุการณ์ปิด บริบทข้อมูลของคุณไม่ใช่หน้าต่าง ... อาจเป็นเทมเพลตข้อมูลที่มีกริดหรือบางอย่างซึ่งไม่มีเหตุการณ์ปิด ดังนั้นคำตอบของ dbkk คือคำตอบที่ดีที่สุดในกรณีนี้ อย่างไรก็ตามฉันชอบวิธีการโต้ตอบ / EventTrigger เมื่อมีเหตุการณ์
NielW

รหัสที่คุณมีจะทำงานได้ดีกับเหตุการณ์ที่โหลดเช่น
NielW

คำตอบ:


126

ฉันเพียงแค่เชื่อมโยงตัวจัดการในตัวสร้างมุมมอง:

MyWindow() 
{
    // Set up ViewModel, assign to DataContext etc.
    Closing += viewModel.OnWindowClosing;
}

จากนั้นเพิ่มตัวจัดการไปที่ViewModel:

using System.ComponentModel;

public void OnWindowClosing(object sender, CancelEventArgs e) 
{
   // Handle closing logic, set e.Cancel as needed
}

ในกรณีนี้คุณจะไม่ได้รับอะไรนอกจากความซับซ้อนโดยใช้รูปแบบที่ซับซ้อนมากขึ้นพร้อมกับการอ้อมมากขึ้น (XAML เพิ่มอีก 5 บรรทัดCommand)

"ศูนย์ behind รหัส" มนต์ไม่ได้เป็นเป้าหมายในตัวเองชี้คือการแยก ViewModel จากมุมมอง แม้เมื่อมีเหตุการณ์ที่ถูกผูกไว้ในรหัสที่อยู่เบื้องหลังของการดูที่ViewModelไม่ได้ขึ้นอยู่กับมุมมองและตรรกะปิดสามารถเป็นหน่วยทดสอบ


4
ฉันเช่นนี้การแก้ปัญหา: ขอแค่เป็นปุ่มที่ซ่อน :)
Benjol

3
สำหรับผู้เริ่มต้น mvvm ที่ไม่ได้ใช้ MVVMLight และค้นหาวิธีแจ้ง ViewModel เกี่ยวกับเหตุการณ์ปิดการเชื่อมโยงวิธีการตั้งค่า dataContext อย่างถูกต้องและวิธีรับวัตถุ viewModel ในมุมมองอาจน่าสนใจ จะรับข้อมูลอ้างอิงไปยัง ViewModel ในมุมมองได้อย่างไร และฉันจะตั้งค่า ViewModel บนหน้าต่างใน xaml ได้อย่างไรโดยใช้คุณสมบัติ datacontext ... ฉันใช้เวลาหลายชั่วโมงในการจัดการเหตุการณ์การปิดหน้าต่างอย่างง่ายใน ViewModel
MarkusEgle

18
โซลูชันนี้ไม่เกี่ยวข้องในสภาพแวดล้อม MVVM โค้ดด้านหลังไม่ควรรู้เกี่ยวกับ ViewModel
จาค็อบ

2
@Jacob ฉันคิดว่าปัญหาเป็นเรื่องที่มากกว่าที่คุณจะได้รับตัวจัดการเหตุการณ์ฟอร์มใน ViewModel ของคุณซึ่งจับคู่ ViewModel กับการใช้ UI ที่เฉพาะเจาะจง หากพวกเขากำลังจะใช้รหัสที่อยู่เบื้องหลังพวกเขาควรตรวจสอบ CanExecute แล้วโทร Execute () ในคุณสมบัติ ICommand แทน
Evil Pigeon

14
@Jacob โค้ดที่อยู่เบื้องหลังสามารถทราบเกี่ยวกับสมาชิก ViewModel ได้ดีเพียงแค่โค้ด XAML ก็ทำได้ หรือคุณคิดว่าคุณทำอะไรเมื่อคุณสร้างคุณสมบัติ Binding to ViewModel? วิธีการแก้ปัญหานี้ดีสำหรับ MVVM ตราบใดที่คุณไม่ได้จัดการกับตรรกะการปิดใน code-behind เอง แต่ใน ViewModel (แม้ว่าจะใช้ ICommand เหมือนที่ EvilPigeon แนะนำอาจเป็นความคิดที่ดีเพราะคุณสามารถผูก ไป)
almulo

81

รหัสนี้ใช้งานได้ดี:

ViewModel.cs:

public ICommand WindowClosing
{
    get
    {
        return new RelayCommand<CancelEventArgs>(
            (args) =>{
                     });
    }
}

และใน XAML:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <command:EventToCommand Command="{Binding WindowClosing}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers>

สมมติว่า:

  • ViewModel ถูกกำหนดให้DataContextกับคอนเทนเนอร์หลัก
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

1
ลืม: เพื่อรับอาร์กิวเมนต์ของเหตุการณ์ในคำสั่งให้ใช้ PassEventArgsToCommand = "True"
Stas

2
+1 วิธีการที่ง่ายและธรรมดา น่าจะดีกว่าถ้าได้มุ่งหน้าสู่ PRISM
Tri Q Tran

16
นี่เป็นสถานการณ์จำลองหนึ่งที่ไฮไลต์ช่องโหว่ใน WPF และ MVVM
ดาเมียน

1
มันจะมีประโยชน์จริง ๆ เมื่อพูดถึงสิ่งที่อยู่iใน<i:Interaction.Triggers>และวิธีการได้รับ
Andrii Muzychuk

1
@Chiz มันเป็นเนมสเปซที่คุณควรประกาศในองค์ประกอบของรากเช่นนี้: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas

34

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

Application.Current.MainWindow.Closing += new CancelEventHandler(MainWindow_Closing);

void MainWindow_Closing(object sender, CancelEventArgs e)
{
            //Your code to handle the event
}

ทั้งหมดที่ดีที่สุด


นี่คือทางออกที่ดีที่สุดในบรรดาที่กล่าวถึงในปัญหานี้ ขอบคุณ !
จาค็อบ

นี่คือสิ่งที่ฉันกำลังมองหา ขอบคุณ!
Nikki Punjabi

20
... และสิ่งนี้จะสร้างการมีเพศสัมพันธ์อย่างแน่นหนาระหว่าง ViewModel และ View -1
PiotrK

6
นี่ไม่ใช่คำตอบที่ดีที่สุด มันทำลาย MVVM
Safiron

1
@ Craig มันต้องมีการอ้างอิงอย่างหนักไปยังหน้าต่างหลักหรือหน้าต่างใดก็จะถูกใช้สำหรับ มันง่ายกว่ามาก แต่นั่นหมายความว่าโมเดลการดูไม่ได้แยกออกจากกัน มันไม่ใช่คำถามของการตอบสนองความต้องการของผู้ใช้ MVVM หรือไม่ แต่ถ้ารูปแบบ MVVM ต้องใช้งานไม่ได้เพื่อให้ใช้งานได้จะไม่มีประโยชน์ในการใช้เลย
อเล็กซ์

16

นี่คือคำตอบตามรูปแบบ MVVM หากคุณไม่ต้องการรู้เกี่ยวกับหน้าต่าง (หรือเหตุการณ์ใด ๆ ) ใน ViewModel

public interface IClosing
{
    /// <summary>
    /// Executes when window is closing
    /// </summary>
    /// <returns>Whether the windows should be closed by the caller</returns>
    bool OnClosing();
}

ใน ViewModel ให้เพิ่มส่วนต่อประสานและการใช้งาน

public bool OnClosing()
{
    bool close = true;

    //Ask whether to save changes och cancel etc
    //close = false; //If you want to cancel close

    return close;
}

ในหน้าต่างฉันเพิ่มเหตุการณ์การปิด โค้ดด้านหลังนี้ไม่ทำลายรูปแบบ MVVM มุมมองสามารถรู้เกี่ยวกับมุมมองโมเดล!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}

เรียบง่ายชัดเจนและสะอาดตา ViewModel ไม่จำเป็นต้องรู้มุมมองที่เฉพาะเจาะจงดังนั้นจึงมีความกังวลอยู่เสมอ
Bernhard Hiller

บริบทเป็นโมฆะเสมอ!
Shahid Od

@ShahidOd ViewModel ของคุณจำเป็นต้องใช้IClosingอินเตอร์เฟสไม่ใช่แค่ใช้OnClosingวิธีการ มิฉะนั้นDataContext as IClosingนักแสดงจะล้มเหลวและกลับมาnull
Erik White

10

Geez ดูเหมือนว่าจะมีรหัสมากมายเกิดขึ้นที่นี่สำหรับเรื่องนี้ Stas ด้านบนมีวิธีการที่เหมาะสมสำหรับความพยายามน้อยที่สุด นี่คือการปรับตัวของฉัน (โดยใช้ MVVMLight แต่ควรเป็นที่จดจำได้) ... โอ้และPassEventArgsToCommand = "True"นั้นแน่นอนสิ่งจำเป็นยิ่งตามที่ระบุไว้ข้างต้น

(ให้เครดิตกับ Laurent Bugnion http://blog.galasoft.ch/archive/2009/10/18/clean-shutdown-in-silverlight-and-wpf-applications.aspx )

   ... MainWindow Xaml
   ...
   WindowStyle="ThreeDBorderWindow" 
    WindowStartupLocation="Manual">



<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding WindowClosingCommand}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers> 

ในรูปแบบมุมมอง:

///<summary>
///  public RelayCommand<CancelEventArgs> WindowClosingCommand
///</summary>
public RelayCommand<CancelEventArgs> WindowClosingCommand { get; private set; }
 ...
 ...
 ...
        // Window Closing
        WindowClosingCommand = new RelayCommand<CancelEventArgs>((args) =>
                                                                      {
                                                                          ShutdownService.MainWindowClosing(args);
                                                                      },
                                                                      (args) => CanShutdown);

ใน ShutdownService

    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void MainWindowClosing(CancelEventArgs e)
    {
        e.Cancel = true;  /// CANCEL THE CLOSE - let the shutdown service decide what to do with the shutdown request
        RequestShutdown();
    }

RequestShutdown มีลักษณะเหมือนดังต่อไปนี้ แต่โดยทั่วไปแล้วขอShutdownหรืออะไรก็ตามที่มีชื่อจะตัดสินใจว่าจะปิดแอปพลิเคชันหรือไม่ (ซึ่งจะปิดหน้าต่างอย่างสนุกสนาน):

...
...
...
    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void RequestShutdown()
    {

        // Unless one of the listeners aborted the shutdown, we proceed.  If they abort the shutdown, they are responsible for restarting it too.

        var shouldAbortShutdown = false;
        Logger.InfoFormat("Application starting shutdown at {0}...", DateTime.Now);
        var msg = new NotificationMessageAction<bool>(
            Notifications.ConfirmShutdown,
            shouldAbort => shouldAbortShutdown |= shouldAbort);

        // recipients should answer either true or false with msg.execute(true) etc.

        Messenger.Default.Send(msg, Notifications.ConfirmShutdown);

        if (!shouldAbortShutdown)
        {
            // This time it is for real
            Messenger.Default.Send(new NotificationMessage(Notifications.NotifyShutdown),
                                   Notifications.NotifyShutdown);
            Logger.InfoFormat("Application has shutdown at {0}", DateTime.Now);
            Application.Current.Shutdown();
        }
        else
            Logger.InfoFormat("Application shutdown aborted at {0}", DateTime.Now);
    }
    }

8

ผู้ถามควรใช้คำตอบ STAS แต่สำหรับผู้อ่านที่ใช้ปริซึมและไม่มี galasoft / mvvmlight พวกเขาอาจต้องการลองสิ่งที่ฉันใช้:

ในคำจำกัดความที่ด้านบนสุดสำหรับหน้าต่างหรือผู้ใช้ควบคุม ฯลฯ กำหนด namespace:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

และใต้คำจำกัดความนั้น:

<i:Interaction.Triggers>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding WindowClosing}" CommandParameter="{Binding}" />
        </i:EventTrigger>
</i:Interaction.Triggers>

อสังหาริมทรัพย์ในมุมมองโมเดลของคุณ:

public ICommand WindowClosing { get; private set; }

แนบผู้แทนคำแนะนำในตัวสร้างมุมมองโมเดลของคุณ:

this.WindowClosing = new DelegateCommand<object>(this.OnWindowClosing);

ในที่สุดโค้ดของคุณที่คุณต้องการเข้าถึงใกล้กับส่วนควบคุม / หน้าต่าง / อะไรก็ตาม:

private void OnWindowClosing(object obj)
        {
            //put code here
        }

3
สิ่งนี้ไม่อนุญาตให้เข้าถึง CancelEventArgs ซึ่งจำเป็นต้องยกเลิกเหตุการณ์ปิด วัตถุที่ผ่านเป็นรูปแบบมุมมองซึ่งในทางเทคนิคเป็นรูปแบบมุมมองเดียวกันกับที่คำสั่ง WindowClosing จะถูกดำเนินการจาก
stephenbayer

4

ฉันจะถูกล่อลวงให้ใช้ตัวจัดการเหตุการณ์ภายในไฟล์ App.xaml.cs ของคุณซึ่งจะช่วยให้คุณตัดสินใจได้ว่าจะปิดแอปพลิเคชันหรือไม่

ตัวอย่างเช่นคุณสามารถมีรหัสต่อไปนี้ในไฟล์ App.xaml.cs ของคุณ:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    // Create the ViewModel to attach the window to
    MainWindow window = new MainWindow();
    var viewModel = new MainWindowViewModel();

    // Create the handler that will allow the window to close when the viewModel asks.
    EventHandler handler = null;
    handler = delegate
    {
        //***Code here to decide on closing the application****
        //***returns resultClose which is true if we want to close***
        if(resultClose == true)
        {
            viewModel.RequestClose -= handler;
            window.Close();
        }
    }
    viewModel.RequestClose += handler;

    window.DataContaxt = viewModel;

    window.Show();

}

จากนั้นภายในรหัส MainWindowViewModel ของคุณคุณสามารถมีสิ่งต่อไปนี้:

#region Fields
RelayCommand closeCommand;
#endregion

#region CloseCommand
/// <summary>
/// Returns the command that, when invoked, attempts
/// to remove this workspace from the user interface.
/// </summary>
public ICommand CloseCommand
{
    get
    {
        if (closeCommand == null)
            closeCommand = new RelayCommand(param => this.OnRequestClose());

        return closeCommand;
    }
}
#endregion // CloseCommand

#region RequestClose [event]

/// <summary>
/// Raised when this workspace should be removed from the UI.
/// </summary>
public event EventHandler RequestClose;

/// <summary>
/// If requested to close and a RequestClose delegate has been set then call it.
/// </summary>
void OnRequestClose()
{
    EventHandler handler = this.RequestClose;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

#endregion // RequestClose [event]

1
ขอบคุณสำหรับคำตอบโดยละเอียด อย่างไรก็ตามฉันไม่คิดว่าวิธีแก้ปัญหาของฉัน: ฉันต้องจัดการกับการปิดหน้าต่างเมื่อผู้ใช้คลิกปุ่ม 'X' ด้านขวาบน มันง่ายที่จะทำในโค้ด - หลัง (ฉันแค่เชื่อมโยงเหตุการณ์การปิดและตั้งค่า CancelEventArgs.Cancel เป็นจริงเท็จ) แต่ฉันต้องการทำเช่นนี้ในรูปแบบ MVVM ขออภัยในความสับสน
Olivier Payen

1

โดยทั่วไปเหตุการณ์หน้าต่างอาจไม่ได้รับมอบหมายให้ MVVM โดยทั่วไปปุ่มปิดแสดงกล่องโต้ตอบเพื่อถามผู้ใช้ "บันทึก: ใช่ / ไม่ใช่ / ยกเลิก" และ MVVM อาจไม่สามารถทำได้

คุณอาจให้ตัวจัดการเหตุการณ์ OnClosing ซึ่งคุณเรียกใช้ Model.Close.CanExecute () และตั้งค่าผลลัพธ์บูลีนในคุณสมบัติเหตุการณ์ ดังนั้นหลังจากการเรียก CanExecute () ถ้าเป็นจริงหรือในเหตุการณ์ OnClosed ให้เรียก Model.Close.Execute ()


1

ฉันไม่ได้ทำการทดสอบมากนัก แต่ดูเหมือนว่าจะใช้งานได้ นี่คือสิ่งที่ฉันมาด้วย:

namespace OrtzIRC.WPF
{
    using System;
    using System.Windows;
    using OrtzIRC.WPF.ViewModels;

    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private MainViewModel viewModel = new MainViewModel();
        private MainWindow window = new MainWindow();

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

            viewModel.RequestClose += ViewModelRequestClose;

            window.DataContext = viewModel;
            window.Closing += Window_Closing;
            window.Show();
        }

        private void ViewModelRequestClose(object sender, EventArgs e)
        {
            viewModel.RequestClose -= ViewModelRequestClose;
            window.Close();
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            window.Closing -= Window_Closing;
            viewModel.RequestClose -= ViewModelRequestClose; //Otherwise Close gets called again
            viewModel.CloseCommand.Execute(null);
        }
    }
}

1
จะเกิดอะไรขึ้นที่นี่ในสถานการณ์ที่ VM ต้องการยกเลิกการปิด
Tri Q Tran

1

เราใช้ AttachedCommandBehavior สำหรับสิ่งนี้ คุณสามารถแนบเหตุการณ์ใด ๆ กับคำสั่งในรูปแบบมุมมองของคุณหลีกเลี่ยงรหัสใด ๆ ที่อยู่เบื้องหลัง

เราใช้มันตลอดการแก้ปัญหาของเราและมีโค้ดเกือบเป็นศูนย์

http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb/


1

การใช้ชุดเครื่องมือ MVVM Light:

สมมติว่ามีคำสั่งExitในรูปแบบมุมมอง:

ICommand _exitCommand;
public ICommand ExitCommand
{
    get
    {
        if (_exitCommand == null)
            _exitCommand = new RelayCommand<object>(call => OnExit());
        return _exitCommand;
    }
}

void OnExit()
{
     var msg = new NotificationMessageAction<object>(this, "ExitApplication", (o) =>{});
     Messenger.Default.Send(msg);
}

สิ่งนี้ได้รับในมุมมอง:

Messenger.Default.Register<NotificationMessageAction<object>>(this, (m) => if (m.Notification == "ExitApplication")
{
     Application.Current.Shutdown();
});

ในทางกลับกันฉันจัดการกับClosingเหตุการณ์MainWindowโดยใช้อินสแตนซ์ของ ViewModel:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ 
    if (((ViewModel.MainViewModel)DataContext).CancelBeforeClose())
        e.Cancel = true;
}

CancelBeforeClose ตรวจสอบสถานะปัจจุบันของมุมมองรูปแบบและคืนค่าจริงถ้าปิดควรหยุด

หวังว่าจะช่วยใครซักคน


-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }

สวัสดีเพิ่มคำอธิบายเล็กน้อยพร้อมกับรหัสเพื่อช่วยให้เข้าใจรหัสของคุณ คำตอบรหัสจะถูกขมวดคิ้วเมื่อ
Bhargav Rao

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