本文章是由機器翻譯。

實際可用性

出現問題時

Charles Kreitzberg 博士和 Ambrose Little

代碼下載位置:MSDN 代碼庫
線上流覽代碼

目錄

可用性挑戰
錯誤消息的意義
設計階段需要考慮的關鍵問題
格式化錯誤消息
程式設計技巧
一致的處理方式
結構良好的錯誤消息
框架
自訂異常(利用 Enterprise Library)

kreitzberg.gif

Charles Kreitzberg

可用性挑戰

從可用性角度來講,錯誤消息常常是惡夢的代名詞。程式出現了錯誤,要讓使用者決定採取何種措施。如果程式產生的錯誤消息能向使用者通告出現了什麼錯誤並指導如何來糾正錯誤,那再好不過。遺憾的是,多數錯誤消息的表現與這一目標相去甚遠。

考慮一下圖 1 中的消息,這是在我的 PC 啟動後不久出現的。想像一下,對於一個對問題一無所知的非技術使用者,這則消息會產生怎樣的影響。這則消息暗示安全已遭到破壞且情況糟糕之極。實際情況並沒有那麼可怕;後來我發現錯誤消息來自我的視頻編輯器軟體,一切完好無損。但這則消息中所包含的設計缺陷讓人瞠目結舌:

  1. 未指出產生錯誤消息的程式。
  2. 該消息未能解釋為什麼終止程式。
  3. 消息表達不清晰。它提到了“安全資訊”,但沒有指出資訊的內容。
  4. 該消息沒有指出問題的嚴重程度以及使用者的電腦是否有風險。
  5. 使用者不知道如何糾正問題或到哪裡尋求更多説明。使用者能做的就是按一下“確定”(實際上什麼都確定不了)。

fig01.gif

圖 1 天哪,要終止運行了

fig02.gif

圖 2 要是 Windows 聯繫我的時候我不在家怎麼辦?

每個人都曾遇到過讓人迷惑不解的錯誤消息。這其中也包括 Microsoft,雖然它曾制訂過標準,對如何創建良好的消息給出了建議,但偶爾也會顯示如圖 2 所示的消息,我收到了好多次這樣的消息。

Windows Vista 使用者體驗指南中包含錯誤消息指導,它指出良好的錯誤消息應主要由三部分構成:

  • 指明出現問題。
  • 解釋原因。
  • 為使用者提供修正問題的解決方案。

它還建議良好的錯誤消息應採用的表現形式是:

  • 相關——消息呈現的問題能引起使用者注意。
  • 有實效——使用者收到消息後或採取行動,或更改其行為。
  • 以使用者為中心——消息依據受眾使用者操作或目標說明問題,而不是以代碼的好惡為重點。
  • 簡潔——消息要盡可能簡短,但不能內容不足。
  • 清晰——消息要使用平實的語言,以便目標使用者能毫不費力地理解問題和解決方案。
  • 具體——消息使用具體的語言說明問題,給出具體的名稱、位置和所涉及物件的價值。
  • 用詞得體——不應讓使用者產生羞愧感。
  • 頻率小——不是經常出現。經常出現的錯誤消息意味著設計不佳。

圖 2中的錯誤消息與上述大部分標準不符。

錯誤消息的意義

最終使用者效率的重要性使得高品質的錯誤消息意義非凡。錯誤可能導致處理中斷、使用者體驗降低並產生額外費用。使用者的支援成本可能數額巨大。良好的錯誤消息可以説明使用者找出問題並加以解決,從而節省大量時間和費用,並將對使用者體驗的影響降至最小。但是,良好的錯誤消息往往不能唾手可得。

根據定義,程式遇到異常情況時會產生錯誤消息。問題的來源可能與檢測到錯誤的點相距甚遠,很難找出原始根源。儘管控制環境通常很難做到,但您可以花些時間設計有用的消息,將技術問題轉化成以使用者為中心且有實效的術語,以此改善境況。

設計階段需要考慮的關鍵問題

下麵列出了一些以使用者為中心的關鍵問題,在計畫異常的管理方式時,您需要對它們加以考慮:

誰是您的受眾?是開發人員、受過培訓的使用者、臨時使用者還是公眾?

您想報告哪些類型的異常?您打算如何表達它們?

使用者將會有何種支援?例如,會有服務台或知識庫支援嗎?

如何説明使用者恢復?僅通知使用者出現了問題,不提供有效的解決辦法,這往往會讓使用者感覺不佳。

您需要將異常情況記入日誌併發送通知嗎?如果是,您可能還需要考慮使用者或 IT 人員查看、搜索、分類、過濾和維護日誌的方式,以及通知的格式和內容。它們應十分便於使用。

是否需要針對不同的受眾設計不同的視圖?例如,專門設計開發人員視圖或公眾視圖是否會有説明?使用外部策略檔控制顯示是否會產生任何安全性漏洞?

您能否提高消息的可用性?您能否通過將異常打包到您新建的異常中保留到基本技術資訊的連結?跨應用程式邊界傳遞技術資訊是否可能攜帶任何安全風險?

應儘早考慮這些問題——不要等到專案臨近結束才考慮。

格式化錯誤消息

錯誤消息應該準確、易於理解且盡可能有實效。它的內容應包括:

  • 盡可能完整的圖片。
  • 盡可能多的環境,並以使用者可以理解的語言表達出來。
  • 清晰給出建議的使用者操作。通過確保使用者理解每種選擇的後果,説明使用者盡可能接受決策。

以下是一些説明您創建良好錯誤消息的建議:

指出根源 確保指明產生錯誤消息的網站或應用程式。如能指出更詳細的資訊説明找出出現錯誤的代碼區域,也會很有益處,但是要注意:如果任何內部細節可能被惡意攻擊利用,不要透露這類資訊。從主消息中分離出附加的診斷和查詢資訊,以免讓最終使用者感覺資訊的技術性過強。考慮根據需要顯示細節(例如在折疊區域),這樣非技術使用者就不必處理技術表達。

使用平實的語言 盡可能使用一般性的語言解釋發生的情況。由於您可能不了解出現錯誤時使用者所做的嘗試,所以可能操作起來比較困難。但是,您將問題與使用者嘗試完成的操作和所波及的資料聯繫得越緊密,越有助於使用者理解並依據資訊內容操作解決問題。仍需注意,不要透露可能被攻擊利用或違反隱私的資訊。說明錯誤的嚴重程度,如可能,說明問題的後果。

提供細節和指導 對於高層級的異常,例如“HTML 錯誤 -500 –內部伺服器錯誤”,嘗試盡可能提供更多細節和具體的指導。說明使用者可以用來糾正錯誤的操作,並確保清晰描述選項和結果。如有更多可説明使用者做出適當決策的資訊,盡可能提供到它們的連結。如果只能建議使用者聯繫支援人員,確保聯繫資訊準確無誤且易於更新。

定向使用者 如果您在 ASP.NET 中使用了自訂錯誤頁面,採用可視方式將其與網站集成到一起,並提供到相關頁面(或者至少是主頁)的導航。此外,如果您將使用者指向包含其他資訊的網站,不要將使用者隨便地放在主頁上並讓他們自己查找資訊,應將其直接放到合適的頁面上。

little.gif

Ambrose Little

程式設計技巧

我承認錯誤消息有難度,Charles 給出的所有建議都是極有説明的。那麼,如何在代碼中實際應用這些建議呢?

從程式設計角度而言,最好將錯誤消息分為兩類。程式性錯誤是由於對環境估計不足導致的代碼異常;出現這類錯誤時,適度終止程式是唯一的選擇。使用者輸入或驗證異常是使用者可能糾正問題的情形,對於這類錯誤,結構良好的錯誤消息會對恢復起到促進作用。在本專欄中,我著重講解程式性錯誤,以後再討論使用者輸入和驗證錯誤。

一致的處理方式

即使是編寫得最好的程式也難免會出現錯誤。最佳處理方式就是為錯誤消息創建可在整個程式中使用的類。通過在單點排列所有錯誤消息處理,您可完成下列工作:

  1. 確保以完整一致的格式表達所有消息。
  2. 使用自訂的異常類型並為每條消息指定唯一的錯誤代碼,以便生成錯誤消息庫、使用其他資訊加以擴展並在適當時進行當地語系化。
  3. 如可能,將錯誤消息連結到提供使用者支援的網站,或至少提供某種方式,供使用者聯繫錯誤消息的應用程式支援。
  4. 創建可以自動或使用使用者許可權發送到伺服器的日誌,可以利用這些日誌分析問題並改進軟體。

結構良好的錯誤消息

34 中的對話方塊顯示了結構良好的錯誤消息中的元素。如果是簡單的錯誤消息,可使用圖 3 中的對話方塊,它摘錄自 Windows Vista 指南。此錯誤消息最適合使用者可糾正的錯誤。程式性錯誤需要更全面的消息,如圖 4 所示。

fig03.gif

圖 3 簡單錯誤消息的格式

fig04.gif

圖 4 更複雜錯誤的消息

MSDN 庫文章“.NET Framework 開發人員指南:異常的設計準則”中對處理異常給出了不錯的說明。如您登錄 MSDN 庫,請查看文章“異常處理應用區塊”。它可以打包異常,將其嵌套在您使用更多資訊創建的異常內。您可利用這一功能添加其他資訊,捕捉產生異常的環境。此資訊能説明您創建更多以使用者為中心的錯誤消息。

如果關注安全性,您可能希望避免傳送技術資訊,在這種情況下,您可以使用以使用者為中心程度更高的資訊替換異常,而不是將其打包。最後,您可以使用“異常處理塊”記錄錯誤並通過電子郵件、Windows Management Instrumentation (WMI) 其他一些自訂的機制來通知相關人員。

如果您在為 ASP.NET 尋找羽量級的錯誤記錄和報告程式,請查看錯誤記錄模組和處理常式(ELMAH)。它提供了記錄功能、幾個日誌存儲選項、在網頁上查看日誌的能力、電子郵件和 RSS 通知。您還可為 EntLib 創建 ELMAH 例外處理常式,例如dotNetTemplar 上的一個免費程式。這樣您就能獲得 EntLib 提供的基礎結構和 ELMAH 提供的報告服務。

如果您是為 Windows XP 或 Windows Vista 環境構建應用程式,可以考慮 Windows 錯誤報告服務 (Dr.Watson)。使用者提交故障資料時,Windows 錯誤報告服務會檢查是否有與其相關的使用者資訊,並在出現故障時顯示消息。

雖然這些框架非常適合管理異常處理,但它們並不能保證可用性。您仍需要通過儘量瞭解使用者對技術的理解並創建最為清晰、準確且有實效的陳述,從使用者角度查看情況。

現在看一個示例和一些演示上面所討論概念的示例代碼。

自訂異常(利用 Enterprise Library)

下麵的內容通過示例講述了如何利用 Enterprise Library 異常處理塊為 Windows 表單應用程式自訂異常;類似技術也可用於其他 Microsoft .NET Framework UI 技術。

首先,通過一些 ExceptionHandling 類集中異常處理,如圖 5 所示。這是一個靜態類,其中的方法可以直接映射成 Enterprise Library 配置中的已定義異常策略。

圖 5 ExceptionHandling 類

public static class ExceptionHandling
{
    public static Exception UiUnknown(Exception exception)
    {
        try
        {
            if (Microsoft.Practices.EnterpriseLibrary.
                ExceptionHandling.ExceptionPolicy
                  .HandleException(exception, "UiUnknown"))
                return exception;
        }
        catch (Exception ex)
        {
            return ex;
        }
        return null;
    }
}

在 Enterprise Library 配置工具中配置該策略,如圖 6 所示。指定兩類處理常式,這與 try/catch 語句十分相似。選擇更具體的處理常式。對於這兩類處理常式,您可以先使用內置的 XML 異常格式化程式和 XML 記錄跟蹤偵聽器記錄拋出的異常。對於普通異常,您可以將源異常打包到帶有以使用者為中心的消息的新異常內。您也可對其進行替換,但僅限 UI 想要擁有實際異常的細節時,儘量還是將其打包。

fig06.gif

圖 6 Enterprise Library 配置工具

在打包或替換時,有一個重要的小技巧,即將異常類型上的 PostHandlingAction 設置為 ThrowNewException——這樣它就能拋出打包後的新異常。

如果您想調用代碼對異常進行更多處理,可以選擇 NotifyRethrow,它會令 EntLib ExceptionPolicy.HandleException 返回真值。如您所見,對於 DatabaseConnectivityException,您可以處理採用自訂異常格式且以使用者為中心的內容,因此,先前的策略只需記錄並通知應重新拋出即可。

通過調用 ExceptionHandling.UiUnknown,您會得到打包的新異常;如果策略未選擇拋出新異常或通知重新拋出,您會得到原始異常或一無所得——這是異常處理塊的主要優點——您可以在應用程式碼的外部配置自己的策略(非常適合在應用程式碼之外維護橫切關注點)。然後將其傳遞給 ErrorMessage.Show 方法,圖 7 中的 ErrorMessage 表單中有它的定義。

圖 7 ErrorMessage 表單

public static void Show(Exception relatedException)
{
    ErrorMessage em = new ErrorMessage();
    if (relatedException == null)
    {
        em.problemDetailsContainer.Visible = false;
    }
    else
    {
        em.problemDescription.Text = relatedException.Message;
        IUIExceptionDetail detail = 
          relatedException as IUIExceptionDetail;
        if (detail == null)
        {
            em.errorCode.Text = "500";
            em.problemDetails.Visible = false;
        }
        else
        {
            em.errorCode.Text = detail.ErrorCode.ToString();
            em.problemDetails.Text = detail.DetailMessage;
        }
        em.problemType.Text = 
          em.GetMeaningfulExceptionType(relatedException).Name;
        em._SearchText = em.errorCode.Text + " " + em.problemType.Text;
    }
    em.ShowDialog();
}

ErrorMessage 表單是個友好螢幕,上面有公司名稱、聯繫資訊且能向公司報告問題並線上搜索解決方案。

如果沒有給出異常,它會隱藏細節容器——不必誤導使用者想像有細節存在。 (在這種情況下,甚至沒有必要顯示消息。 )如果確實有異常,將問題說明設置為異常消息。 然後檢查異常類型是否執行了 IUIExceptionDetail interface。

此介面讓您可以在條件允許的情況下有針對性為使用者提供更多有意義的異常資訊。 介面如下所示:

public interface IUIExceptionDetail
{
    int ErrorCode { get; }
    string DetailMessage { get; }
}

使用這一介面,您可以創建以使用者為中心的自訂異常,這種有用的資訊可以説明使用者嘗試自己解決問題,或至少提供良好的錯誤代碼,為搜索和支援開啟方便之門。 此介面的示例實現是 DataConnectivityException,如圖 8 所示。

圖 8 DataConnectivityException

public class DatabaseConnectivityException 
  : Exception, IUIExceptionDetail
{
    public DatabaseConnectivityException(Exception innerException)
     : base(Resources.Exceptions.DatabaseConnectivityException_Message, 
                innerException)
    {
        int.TryParse(
          Resources.Exceptions.DatabaseConnectivityException_Code,
          out _ErrorCode);
        _DetailMessage = 
           Resources.Exceptions
             .DatabaseConnectivityException_DetailMessage;
    }

    #region IUIExceptionDetail Members
    int _ErrorCode;
    public int ErrorCode
    {
        get { return _ErrorCode; }
    }

    string _DetailMessage;
    public string DetailMessage
    {
        get { return _DetailMessage; }
    }
    #endregion
}

最有趣的地方是使用 RESX 檔存儲消息、錯誤代碼和細節。這樣有利於進行當地語系化,還能成為錯誤消息文檔的便利 XML 源——您可以輕鬆將 XSLT 應用於 RESX 格式,以便生成某些 HTML 或其他富文本標記。

您會發現此方法很適合 EntLib 提供的多種基本打包/替換工具,不錯吧——您不一定非要使用 EntLib 提供以使用者為中心的消息,但將異常處理形象化為策略以便記錄不失為一個好方法,您至少還可以在應用程式外自訂異常處理。

再來看看 ErrorMessage.Show 方法,其中檢查了 IUIExceptionDetail 介面。如果找到,它使用額外提供的資訊顯示給使用者。否則,它隱藏細節框並顯示常規錯誤代碼 500(類似 HTTP 500)。

常規/未知錯誤的結果如圖 9 所示。對於更具體的錯誤,如 DataConnectivityException,其消息如圖 10 所示。

fig09.gif

圖 9 常規錯誤消息

fig10.gif

圖 10 更多具體錯誤消息

此處有幾個需要注意的事項。首先,它在很大程度上遵循了 Windows Vista 對錯誤消息的指導方針。視窗是一個強制回應對話方塊。對話方塊的標題清晰地告訴使用者是什麼產生的錯誤,這樣,使用者就不會誤認為是系統錯誤;這也可以是具體的應用程式功能。錯誤有一個明顯的標記,一個位於應用程式徽標上的大圖示,既強調了問題的來源,又清晰地指出它是個錯誤。標題文字也明確告訴使用者應用程式出現問題。

標題正下方顯示了異常消息。它應該簡潔且用詞得體,使用使用者可以理解的語句。如可能,它應提供解決方案。如果是一般性錯誤,您可能不了解內情,因此最好的方式是讓使用者報告錯誤或執行線上搜索。如果是連線性問題,您要是知道合適的解決方案,可以額外提供些建議。原則是鼓勵使用者自己解決問題,這就是常規消息最好要具體的原因。

fig11.gif

圖 11 “描述問題”是重要的按鈕 Ambrose Little

“線上搜索解決方案”連結按鈕將使用 "MSDN Magazine"、錯誤和異常類型執行搜索。這是一個相當不錯的功能,因為使用者可能不知道在搜索框中輸入什麼內容。如果您的網站有使用者支援區,您還可以將搜索限定在網站上。當然,如果您有具體的查詢服務(視代碼而定),您應首先選用這類服務,而不是普通的關鍵字搜索。

“描述問題”按鈕應將 XML 日誌發送給 Web 服務。您可能還想讓使用者提供任何環境說明和/或聯繫資訊,服務應返回某些跟蹤資訊,如果使用者選擇調用,可以用這些資訊做為參考。

圖 11 顯示了您可使用的另一種表現形式。它的優點是“描述問題”是螢幕上最顯著的命令。在設計 UI 時,需要考慮到根據視覺層次指導使用者,換言之,如果有某些事非常重要,或者在本例中,您想要鼓勵使用者使用某條命令,應確保它最突出、明顯且易用。您必須根據環境決定最重要的操作,如果有層次結構,用可視方式將其表達出來。

一旦這些準備就緒,您就可以在 Program.cs Main 方法中添加應用程式級的處理常式,如下所示:

Application.ThreadException += Application_ThreadException;

該方法如下所示:

static void Application_ThreadException(object sender, 
  System.Threading.ThreadExceptionEventArgs e)
{
    ErrorMessage.Show(ExceptionHandling.UiUnknown(e.Exception));
}

它可捕捉所有意外 UI 異常,並通過您核心例外處理常式類中的 "UiUnknown" 策略路由異常,在本例中它授權處理 Enterprise Library,雖然它可以是您所擁有的任何異常處理架構。該方法可能返回一個異常,通過您設計的對使用者更友好的 ErrorMessage 表單顯示出來。當然,您可將類似的方式用於整個應用程式中更具體的異常策略。

如您所見,創建良好的錯誤消息絕非易事,但如能花些時間認真規劃其結構,可以為使用者節省大量精力並減少挫折。

Charles Kreitzberg 博士是Cognetics Corporation的 CEO,該公司提供可用性諮詢和使用者體驗設計服務。他的愛好是創建直觀的介面,使使用者既能通過產品完成業務工作,又能獲得視覺上的享受。Charles 居住在新澤西中部,兼職做演藝歌手。

Ambrose Little 與妻子和四個孩子也居住在新澤西中部。他從事軟體設計與開發已經十多年了,是受人尊敬的 INETA 發言人和 Microsoft MVP。最近,他從技術設計轉為以人為中心的設計,現在是 Infragistics 的一名使用者體驗設計師。