具備事件(Events)的物件在進行序列化(Serialization)過程中的問題

ORM的程式設計觀念在現代已經是大家普遍使用的技巧了,但是用來塑造ORM的類別,往往我們會賦予給他更多的非資料使用方式,例如可能會有一堆方法,甚至是事件的觸發。ClassType複雜歸複雜,但是總是運作的良好得當。BUT,人生最重要的就是這個BUT,當我們可能需要把這個類別狀態保留時(Application、Session、ViewState...或其他提供序列化Cache服務的機制),就需要序列化它,而事件若有被外部調用掛載,這時候你就會遭遇到莫名其妙的問題了。

這個問題的中英文描述如下,大致上就是在跟你講某一個類別物件不可序列化:

無法序列化工作階段狀態。在 'StateServer' 和 'SQLServer' 模式中,ASP.NET 將序列化工作階段狀態物件,因此不允許無法序列化的物件或 MarshalByRef 物件。在 'Custom' 模式中,自訂工作階段狀態存放區執行類似的序列化作業時,也會有同樣的限制。 

Unable to serialize the session state. Please note that non-serializable objects or MarshalByRef objects are not permitted when session state mode is 'StateServer' or 'SQLServer'.

其實這個問題有很多種解決方法,最常見的大概就是程式設計正規化魔人跟你說的,ORM物件就是ORM物件,你不要汙染它好嗎?請重構Brabra...說的是沒錯啦,但每個人有每個人的人生觀,畢竟這個系統現在是能動的沒錯吧?程式碼大致上也呈現了結構化而不是髒亂毫無章法沒錯吧?你要把人生浪費在Coding,我要把人生浪費在我覺得更美好的事物上,誰對誰錯?

先來看出錯的程式碼:

ORM_EventClass.cs

namespace EventTest
{
  public delegate void EventHandler(object sender, EventArgs e);

  [Serializable]
  public class ORM_EventClass
  {
    public string cName { get; set; }
    public string cPhone { get; set; }
    
    public event EventHandler OnWriteFinish;
    
    public void WriteToDatabase()
    {
      //BraBra... Do data write ...
      if (OnWriteFinish != null) { OnWriteFinish(this, new EventArgs()); }
    }
  }
}

Test.aspx(錯誤會發生在Session回存調用時,理由是OnWriteFinish事件掛載方法無法序列化)

<script runat="server">
  public void Page_Load(object sender, EventArgs e)
  {
    EventTest.ORM_EventClass oTemp = new EventTest.ORM_EventClass();
    oTemp.cName  = "王小明";
    oTemp.cPhone = "0912345678";
    oTemp.OnWriteFinish += ShowSuccess;  //Can NOT serializable
    oTemp.WriteToDatabase();
    Session["JustTest"] = oTemp;
  }

  public void ShowSuccess(Object sender, EventArgs e)
  {
    uMsg.Text = "寫入成功。";
  }
</script>

解決方案A

重構的那件事情就不要再提了,誰有那種美國時間改牽一髮動全身的Code啊!答案就是在寫入Session前,把事件綁定卸除。蠢!但很有效!

<script runat="server">
  public void Page_Load(object sender, EventArgs e)
  {
    EventTest.ORM_EventClass oTemp = new EventTest.ORM_EventClass();
    oTemp.cName  = "王小明";
    oTemp.cPhone = "0912345678";
    oTemp.OnWriteFinish += ShowSuccess;  //Can NOT serializable
    oTemp.WriteToDatabase();
    //回存Session前先卸除
    oTemp.OnWriteFinish -= ShowSuccess; //Stupid! But it works.
    Session["JustTest"] = oTemp;
  }

  public void ShowSuccess(Object sender, EventArgs e)
  {
    uMsg.Text = "寫入成功。";
  }
</script>

解決方案B

另外有一個解決方案就是對Class動刀,就是使用[NonSerialized]屬性來叫.NET不要幫你做事件掛載點這部分的序列化工作,但是這邊有雷,當我們把這個屬性(Attributes)鍵入下去的時候,會中另外一個雷,程式碼如下:

namespace EventTest
{
  public delegate void EventHandler(object sender, EventArgs e);

  [Serializable]
  public class ORM_EventClass
  {
    public string cName { get; set; }
    public string cPhone { get; set; }
    
    [NonSerialized]
    public event EventHandler OnWriteFinish;
    
    public void WriteToDatabase()
    {
      //BraBra... Do data write ...
      if (OnWriteFinish != null) { OnWriteFinish(this, new EventArgs()); }
    }
  }
}

編譯器會跟你講無法編譯,文字資訊如下:

屬性 'NonSerialized' 在此宣告類型上無效。它只在 'field' 宣告上有效。

Attribute 'NonSerialized' is not valid on this declaration type. It is only valid on 'field' declarations.

這部分要正式的解法其實很曲折,不符合本人簡單解決事情的習慣,因此在這邊教大家一個小撇步來解決:

[field: NonSerialized]
public event EventHandler OnWriteFinish;

Yes,收工!繼續快樂過生活。

請注意,當你採用解決方案B時,就不需要再使用解決方案A:將事件移除的方法喔。

ASP.NET Session Application ViewState ClassTypeSerialization Serialized NonSerialized Attributes Properties