跳至页脚内容
Iron Academy Logo
学习 C#
学习 C#

其他类别

了解 C# 事件

Tim Corey
1h 9m 13s

C# 中的事件是一个基本概念,许多开发人员都在使用,但可能并没有完全理解,尤其是在创建自己的事件时。 Tim Corey 在他的视频" C# 事件 - 在应用程序中创建和使用事件"中提供了关于如何创建和使用事件的全面指南,探讨了最佳实践和高级功能。

在本文中,我们将探讨 C# 事件,重点介绍它们的语法、定义方式以及如何使用 Tim Corey 的视频示例来帮助解决常见的编程问题。 理解 C# 中的事件处理对于构建响应式应用程序至关重要,本文将帮助您掌握自定义错误日志记录、异常处理和事件驱动编程的核心概念。

活动介绍

在 C# 和其他编程语言中,事件在事件驱动编程中起着至关重要的作用,使应用程序能够响应用户交互或系统更改等操作。 与其他编程语言相比,C# 提供了一种结构化的事件处理方法,使其成为快速小型应用程序的理想选择。 C# 中的事件由特定操作触发,这些操作会调用相应的事件处理程序来执行所需的任务。

Tim 首先解释说,大多数开发人员都熟悉 C# 中的事件,但可能不知道如何创建自定义事件。 该视频的目标是介绍事件,逐步讲解如何创建自定义事件,讨论其功能,并概述最佳实践。

应用程序演示

Tim 使用一个用 Windows Forms (WinForms) 构建的简单银行应用程序来演示事件。 该应用程序模拟一位客户的基本银行操作,包括查看余额和记录交易。

1.应用概述

  • 该应用程序有两个表单:一个用于显示账户余额和交易记录,另一个用于记录新的交易记录。
  • 它包含模拟信用卡购物和通过将资金从储蓄账户转移到支票账户来处理透支的按钮。

2.主要形式

  • 显示客户姓名、支票账户和储蓄账户余额。
  • 显示两个账户的交易列表。
  • 包含一个用于打开交易记录表单的按钮。

3.交易表格

  • 允许用户输入交易金额。
  • 模拟购物,并通过将资金从储蓄账户转移到支票账户来处理透支。

演示应用程序背后的代码

Tim 解释了演示银行应用程序的后端代码:

1.客户类别

  • 代表客户,具有客户姓名和两个账户(支票账户和储蓄账户)的属性。
  • Customer 类虽然简单,但却是理解如何管理多个帐户的良好起点。

2.账户类别

  • 管理银行账户的详细信息,包括账户名称、余额和交易列表。
  • Balance 属性的类型为 decimal,用于进行精确的货币计算。
  • Transactions 属性是一个只读列表,以防止外部修改。

3.处理存款和付款

  • AddDeposit 方法:向账户添加存款,更新余额,并记录交易。
  • MakePayment 方法:处理提款,包括检查资金是否充足,并在必要时通过从备用账户转账来管理透支保护。

4.透支保护

  • 该应用程序包含处理透支的逻辑。 如果支票账户余额不足以进行交易,应用程序会检查储蓄账户以弥补差额。 如果合并余额充足,它会将所需金额转入支票账户并完成交易。

有关代码示例,您可以直接参考 Tim Corey 的视频,他在 3:18 到 18:27 处解释了代码。

事件:按钮点击

在本节中,Tim Corey 深入探讨了 Windows Forms 中按钮点击事件的工作原理,解释了这些事件是如何连接的以及它们是如何工作的。

了解按钮点击事件

Tim 首先解释了 Windows 窗体应用程序中按钮单击事件这一常见概念。 在设计器中双击按钮时,Visual Studio 会自动为单击事件生成事件处理程序方法。

1.事件处理方法

  • 每当按钮被点击时,都会调用此方法。 这里是放置响应按钮点击而运行的代码的地方。

    private void recordTransactionButton_Click(object sender, EventArgs e)
    {
       // Code to handle the button click event
    }
    private void recordTransactionButton_Click(object sender, EventArgs e)
    {
       // Code to handle the button click event
    }

2.活动筹备

  • 事件处理方法与窗体设计器文件(FormName.Designer.cs)中的按钮单击事件关联。 这是通过 += 运算符将事件处理程序添加到按钮的点击事件中来实现的。

    this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);
    this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);

创建和调用自定义事件

Tim 解释了如何在 C# 中创建自定义事件,首先从 Account 类开始,事件将在该类中触发。

1.定义事件

  • 使用 event 关键字定义事件。 它看起来类似于公共变量,但专门用于事件。

    public event EventHandler<string> RaiseTransactionApprovedEvent;
    public event EventHandler<string> RaiseTransactionApprovedEvent;

2.触发事件

  • 事件通常通过 Invoke 方法触发,该方法一般位于定义事件的类中。 Invoke方法接受两个参数:发送者(通常为this)和事件数据(在这种情况下为字符串)。

    private void OnTransactionApproved(string transactionName)
    {
       RaiseTransactionApprovedEvent?.Invoke(this, transactionName);
    }
    private void OnTransactionApproved(string transactionName)
    {
       RaiseTransactionApprovedEvent?.Invoke(this, transactionName);
    }

3.连接和处理事件

  • 使用 += 运算符订阅事件,并定义一个方法来处理引发的事件。

    account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent;
    
    private void Account_RaiseTransactionApprovedEvent(object sender, string e)
    {
       // Code to handle the event
    }
    account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent;
    
    private void Account_RaiseTransactionApprovedEvent(object sender, string e)
    {
       // Code to handle the event
    }

使用事件更新用户界面

Tim 解释了如何使用事件在发生某些操作时(例如交易获得批准时)自动更新浏览器 UI。

1.筹款活动

  • 交易获得批准后触发自定义事件。

    public void AddDeposit(decimal amount, string depositName)
    {
       // Code to add deposit
       RaiseTransactionApprovedEvent?.Invoke(this, depositName);
    }
    public void AddDeposit(decimal amount, string depositName)
    {
       // Code to add deposit
       RaiseTransactionApprovedEvent?.Invoke(this, depositName);
    }

2.在用户界面中订阅事件

  • 在主表单中订阅该事件,以便在新交易获得批准时更新交易列表。

    public MainForm()
    {
       InitializeComponent();
       account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent;
    }
    
    private void Account_RaiseTransactionApprovedEvent(object sender, string e)
    {
       // Update the UI with the new transaction
    }
    public MainForm()
    {
       InitializeComponent();
       account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent;
    }
    
    private void Account_RaiseTransactionApprovedEvent(object sender, string e)
    {
       // Update the UI with the new transaction
    }

事件调用 (Event?.Invoke()) 的解释

Tim Corey 解释了在 C# 中引发事件时如何使用 ?.Invoke() 语法。 这种现代方法简化了代码并确保了线程安全。

问号运算符

Invoke 前面的问号(?.)是空条件运算符。 它会在调用事件处理程序之前检查它是否为空,从而防止出现潜在的异常。

1.传统方法

  • 以前,开发人员需要使用多行代码来检查事件处理程序是否为空,然后再调用它。

    if (RaiseTransactionApprovedEvent != null)
    {
       RaiseTransactionApprovedEvent(this, depositName);
    }
    if (RaiseTransactionApprovedEvent != null)
    {
       RaiseTransactionApprovedEvent(this, depositName);
    }

2.使用 ?.Invoke() 的现代方法

  • 空条件运算符简化了此过程,在一行中执行空值检查并调用事件。

    RaiseTransactionApprovedEvent?.Invoke(this, depositName);
    RaiseTransactionApprovedEvent?.Invoke(this, depositName);
  • 如果RaiseTransactionApprovedEvent为null,则不执行调用,有效地避免任何异常。

3.益处

-简化代码:减少安全调用事件所需的代码量。 -线程安全:通过检查空值并在一步中调用事件来消除竞争条件。

聆听并为活动编写代码

Tim 讲解了如何在 Windows 窗体应用程序中监听自定义事件并相应地更新 UI。

1.订阅活动

  • 使用 += 运算符订阅自定义事件。

    customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;
    customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;

2.事件处理方法

  • 定义一个处理该事件的方法。 此方法根据事件数据更新用户界面。

    private void CheckingAccount_TransactionApprovedEvent(object sender, string e)
    {
       // Update the UI with the new transaction
       checkingTransactionsDataSource.DataSource = null;
       checkingTransactionsDataSource.DataSource = customer.CheckingAccount.Transactions;
       checkingBalanceLabel.Text = customer.CheckingAccount.Balance.ToString("C2");
    }
    private void CheckingAccount_TransactionApprovedEvent(object sender, string e)
    {
       // Update the UI with the new transaction
       checkingTransactionsDataSource.DataSource = null;
       checkingTransactionsDataSource.DataSource = customer.CheckingAccount.Transactions;
       checkingBalanceLabel.Text = customer.CheckingAccount.Balance.ToString("C2");
    }

创建自定义事件:事件执行与回顾

Tim 实际演示了创建、发起和处理自定义事件的整个过程。

1.触发事件

  • 交易获得批准或余额发生变化时触发事件。

    private void OnTransactionApproved(string transactionName)
    {
       RaiseTransactionApprovedEvent?.Invoke(this, transactionName);
    }
    private void OnTransactionApproved(string transactionName)
    {
       RaiseTransactionApprovedEvent?.Invoke(this, transactionName);
    }

2.处理多个事件

  • 确保应用程序监听多个事件,例如交易和余额变化,以保持用户界面实时更新。

    public MainForm()
    {
       InitializeComponent();
       customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;
       customer.SavingsAccount.TransactionApprovedEvent += SavingsAccount_TransactionApprovedEvent;
    }
    
    private void SavingsAccount_TransactionApprovedEvent(object sender, string e)
    {
       // Update the UI with the new savings transaction
       savingsTransactionsDataSource.DataSource = null;
       savingsTransactionsDataSource.DataSource = customer.SavingsAccount.Transactions;
       savingsBalanceLabel.Text = customer.SavingsAccount.Balance.ToString("C2");
    }
    public MainForm()
    {
       InitializeComponent();
       customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;
       customer.SavingsAccount.TransactionApprovedEvent += SavingsAccount_TransactionApprovedEvent;
    }
    
    private void SavingsAccount_TransactionApprovedEvent(object sender, string e)
    {
       // Update the UI with the new savings transaction
       savingsTransactionsDataSource.DataSource = null;
       savingsTransactionsDataSource.DataSource = customer.SavingsAccount.Transactions;
       savingsBalanceLabel.Text = customer.SavingsAccount.Balance.ToString("C2");
    }

3.运行应用程序

  • Tim 运行该应用程序,演示 UI 如何根据事务触发的事件自动更新。

事件参数信息:调试

Tim 展示了如何调试事件并检查随事件发送的信息。

1.设置断点

  • 在事件处理方法中设置断点以检查事件数据。

    private void CheckingAccount_TransactionApprovedEvent(object sender, string e)
    {
       // Breakpoint here
    }
    private void CheckingAccount_TransactionApprovedEvent(object sender, string e)
    {
       // Breakpoint here
    }

2.调试

  • 运行应用程序并执行事务以触发事件。 在调试器中检查事件参数。
  • sender 参数提供引发事件的实例,e 参数包含事件数据。

创建另一个自定义事件(透支事件)

Tim Corey 在演示的基础上,创建了另一个自定义事件来处理透支情况。 当发生透支时,此事件将被触发,并通知用户透支金额。

定义透支事件

Tim 首先在 Account 类中定义了一个用于透支场景的新事件:

1.事件声明

  • 使用 event 关键字定义事件,并将类型设置为 decimal,以传递透支金额。

    public event EventHandler<decimal> OverdraftEvent;
    public event EventHandler<decimal> OverdraftEvent;

2.触发事件

  • 该事件在代码中透支成功发生的部分被触发。

    if (overdraftSuccessful)
    {
       OverdraftEvent?.Invoke(this, overdraftAmount);
    }
    if (overdraftSuccessful)
    {
       OverdraftEvent?.Invoke(this, overdraftAmount);
    }

在仪表盘侧订阅透支事件

1.活动筹备

  • 在仪表盘表单中订阅透支事件。

    customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent;
    customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent;

2.事件处理

  • 定义一个事件处理程序,以便在发生透支时显示消息。

    private void CheckingAccount_OverdraftEvent(object sender, decimal e)
    {
       errorMessage.Text = $"You had an overdraft protection transfer of {e:C2}";
       errorMessage.Visible = true;
    }
    private void CheckingAccount_OverdraftEvent(object sender, decimal e)
    {
       errorMessage.Text = $"You had an overdraft protection transfer of {e:C2}";
       errorMessage.Visible = true;
    }

3.触发和显示事件

  • 当发生透支时,该事件会触发并更新用户界面以通知用户。

    You had an overdraft protection transfer of $20.44

在多个地点监听事件

蒂姆解释了如何用多种语言和形式收听同一事件,展示了事件的多样性。

1.向另一个表单添加标签

  • 在辅助表单中添加标签以显示透支消息。

    <asp:Label ID="errorMessage" runat="server" Visible="false" />
    <asp:Label ID="errorMessage" runat="server" Visible="false" />
    HTML

2.订阅活动

  • 在辅助表单中订阅透支事件。

    customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent;
    customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent;

3.事件处理

  • 定义一个事件处理程序,以便在辅助表单上显示透支消息。

    private void SecondaryForm_OverdraftEvent(object sender, decimal e)
    {
       errorMessage.Visible = true;
    }
    private void SecondaryForm_OverdraftEvent(object sender, decimal e)
    {
       errorMessage.Visible = true;
    }

4.并发事件处理

  • Tim 证明,可以同时以多种形式处理该事件,确保应用程序的所有相关部分都能对该事件做出适当的响应。

    Both the main form and the secondary form display the overdraft message when the event is triggered.

从内存中移除事件监听器

Tim 强调了清理事件监听器对于防止内存泄漏和确保应用程序正常运行的重要性。

1.取消订阅活动

  • 在销毁类实例或关闭表单之前,务必取消订阅事件。

    customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;
    customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;

    为什么这很重要

  • 如果未能取消订阅事件,可能会导致内存泄漏,因为监听事件的对象可能无法被正确地进行垃圾回收。

2.使用命名方法

  • 避免使用匿名函数作为事件处理程序,因为这会使取消订阅事件变得困难。

    // Good practice: using named methods for event handlers
    // Good practice: using named methods for event handlers

通用事件处理程序:为 T 传递类

Tim Corey 解释了通过事件传递数据的最佳实践,特别是使用类而不是像字符串或十进制数这样的简单数据类型的好处。

为什么使用类来存储事件数据

Tim 首先讨论了为什么使用简单数据类型来表示事件不太常见,以及为什么通常最好传递一个类:

1.灵活性和可扩展性

使用类可以通过事件传递多个相关数据。 如果以后需要添加更多数据,只需扩展该类,而无需更改事件的签名。

  1. EventArgs 继承

    • 过去,任何通过事件传递的对象都必须继承自 EventArgs,但现在情况已不再如此。 但是,继承 EventArgs 仍然有利于保持一致性和清晰度。

创建透支事件参数类

Tim 演示了如何为透支事件创建自定义 EventArgs 类。

1.定义类

  • 创建一个继承自 EventArgs 的新类,并包含用于通过事件传递的数据的属性。

    public class OverdraftEventArgs : EventArgs
    {
       public decimal AmountOverdrafted { get; private set; }
       public string MoreInfo { get; private set; }
    
       public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo)
       {
           AmountOverdrafted = amountOverdrafted;
           MoreInfo = moreInfo;
       }
    }
    public class OverdraftEventArgs : EventArgs
    {
       public decimal AmountOverdrafted { get; private set; }
       public string MoreInfo { get; private set; }
    
       public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo)
       {
           AmountOverdrafted = amountOverdrafted;
           MoreInfo = moreInfo;
       }
    }

2.在事件中使用类

  • 更新事件声明,使用自定义的 EventArgs 类。

    public event EventHandler<OverdraftEventArgs> OverdraftEvent;
    public event EventHandler<OverdraftEventArgs> OverdraftEvent;

3.触发事件

  • 调用事件时,传递自定义 EventArgs 类的实例。

    OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));
    OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));

只读属性的重要性

Tim 强调在 EventArgs 类中使用只读属性的重要性,以防止意外修改事件数据。

1.避免修改

  • 如果 EventArgs 类中的属性具有公共 setter,则任何事件处理程序都可以修改数据,这可能会导致意外行为。
  • 使用私有 setter 并通过构造函数初始化属性可确保事件数据保持一致。

2.问题示例

  • Tim 演示了如果属性不是只读的,那么在一个事件处理程序中修改事件数据会如何影响其他处理程序。

    private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e)
    {
       e.AmountOverdrafted = 1000; // This modification affects all handlers
    }
    private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e)
    {
       e.AmountOverdrafted = 1000; // This modification affects all handlers
    }

3.解决方案

  • 使用私有 setter 并通过构造函数传递数据,使属性变为只读。

    public decimal AmountOverdrafted { get; private set; }
    public string MoreInfo { get; private set; }
    public decimal AmountOverdrafted { get; private set; }
    public string MoreInfo { get; private set; }

例外情况:何时使用公共集(59:29)

Tim Corey 指出了使用私有 setter 来设置事件数据属性这一规则的一个重要例外。 当需要允许事件监听器修改事件数据时,例如根据某些条件取消交易时,则属于例外情况。

示例:取消交易

Tim 举了一个例子,说明事件处理程序可能需要取消交易。 这是通过向自定义 EventArgs 类添加 CancelTransaction 属性来实现的。

1.定义属性

  • 向 EventArgs 类添加一个具有 getter 和 setter 的公共属性。

    public class OverdraftEventArgs : EventArgs
    {
       public decimal AmountOverdrafted { get; private set; }
       public string MoreInfo { get; private set; }
       public bool CancelTransaction { get; set; } = false;
    
       public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo)
       {
           AmountOverdrafted = amountOverdrafted;
           MoreInfo = moreInfo;
       }
    }
    public class OverdraftEventArgs : EventArgs
    {
       public decimal AmountOverdrafted { get; private set; }
       public string MoreInfo { get; private set; }
       public bool CancelTransaction { get; set; } = false;
    
       public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo)
       {
           AmountOverdrafted = amountOverdrafted;
           MoreInfo = moreInfo;
       }
    }

2.在事件处理程序中设置属性

  • 在仪表盘中,事件处理程序可以设置此属性以取消交易。

    private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e)
    {
       if (denyOverdraft.Checked)
       {
           e.CancelTransaction = true;
       }
       errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}";
       errorMessage.Visible = true;
    }
    private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e)
    {
       if (denyOverdraft.Checked)
       {
           e.CancelTransaction = true;
       }
       errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}";
       errorMessage.Visible = true;
    }

3.检查源方法中的属性

  • 在触发事件的方法中,在继续之前检查 CancelTransaction 属性是否设置为 true。

    if (args.CancelTransaction)
    {
       return false; // Transaction is canceled
    }
    if (args.CancelTransaction)
    {
       return false; // Transaction is canceled
    }

让应用程序更具互动性

Tim进一步完善了应用程序,使其更具互动性和用户友好性。

1.添加透支控制复选框

  • 在表单中添加一个复选框,允许用户启用或禁用透支保护。

    private void InitializeComponent()
    {
       this.denyOverdraft = new System.Windows.Forms.CheckBox();
       // Initialize other controls
       this.denyOverdraft.Text = "Stop Overdrafts";
       this.denyOverdraft.CheckedChanged += new System.EventHandler(this.denyOverdraft_CheckedChanged);
    }
    private void InitializeComponent()
    {
       this.denyOverdraft = new System.Windows.Forms.CheckBox();
       // Initialize other controls
       this.denyOverdraft.Text = "Stop Overdrafts";
       this.denyOverdraft.CheckedChanged += new System.EventHandler(this.denyOverdraft_CheckedChanged);
    }

2.处理复选框状态

  • 在复选框的事件处理程序中,更新逻辑,以便在决定取消交易时考虑复选框的状态。

    private void denyOverdraft_CheckedChanged(object sender, EventArgs e)
    {
       if (denyOverdraft.Checked)
       {
           // Logic to stop overdraft transactions
       }
    }
    private void denyOverdraft_CheckedChanged(object sender, EventArgs e)
    {
       if (denyOverdraft.Checked)
       {
           // Logic to stop overdraft transactions
       }
    }

3.更新事件处理程序

  • 确保事件处理程序遵循复选框状态,以允许或拒绝透支。

    private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e)
    {
       if (denyOverdraft.Checked)
       {
           e.CancelTransaction = true;
       }
       errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}";
       errorMessage.Visible = true;
    }
    private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e)
    {
       if (denyOverdraft.Checked)
       {
           e.CancelTransaction = true;
       }
       errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}";
       errorMessage.Visible = true;
    }

总结

Tim Corey 在教程的最后总结了在 C# 中使用事件的关键点和最佳实践。

1.移除事件监听器

  • 为防止内存泄漏,在销毁对象之前务必移除事件监听器。
  • 使用 -= 运算符取消订阅事件。

    customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;
    customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;

2.使用EventArgs继承

  • 虽然并非强制性的,但继承 EventArgs 有利于保持一致性并使用 EventArgs.Empty 等内置功能。

3.只读属性的私有设置器

  • 使用私有 setter 防止对事件数据进行意外修改。 仅在必要时才允许公开设置者,例如可取消的交易。

4.事件处理程序语法

  • 使用EventHandler委托来定义事件,提供一种清晰且一致的模式以传递事件数据。

5.空条件运算符

  • 使用空条件运算符 (?.Invoke()) 可以安全地调用事件,而不会冒着出现空引用异常的风险。

结论

Tim Corey 的C# 事件综合教程提供了创建、处理和管理事件的有效方法,并提供了宝贵的见解和实用示例。 遵循这些最佳实践,开发人员可以创建更具交互性和响应性的应用程序。

Hero Worlddot related to 了解 C# 事件
Hero Affiliate related to 了解 C# 事件

分享您的所爱,赚取更多收入

您为使用 .NET、C#、Java、Python 或 Node.js 的开发人员创建内容吗?将您的专业知识转化为额外收入!

钢铁支援团队

我们每周 5 天,每天 24 小时在线。
聊天
电子邮件
打电话给我