将上下文与UI应用程序中抛出的异常相关联的建议方法是什么?

时间:2011-05-04 19:26:52

标签: c# design-patterns error-handling exception-handling

我有一个UI应用程序,它访问数据库,还必须能够对文件执行各种操作。毋庸置疑,在申请过程中可能会抛出各种/众多例外情况,例如:

  • 数据库已脱机。
  • 找不到该文件(以前扫描到数据库中)。
  • 该文件被另一个用户/进程锁定,无法访问。
  • 设置了文件的只读属性,无法修改文件。
  • 安全权限拒绝访问该文件(读取或写入)。

在引发异常时,已知错误的确切细节。但是,有时您需要让异常在调用堆栈中被捕获,以包含异常的上下文,以便您可以创建并呈现用户友好的错误消息;例如在文件复制,文件移动或文件删除操作期间,可能会遇到被另一个进程锁定的文件。

让我们说出于讨论目的,我们有一个必须对文件执行各种操作的方法;它必须将文件读入内存,修改数据并将数据写回,如下例所示:

private void ProcessFile(string fileName)
{
    try
    {
        string fileData = ReadData(fileName);

        string modifiedData = ModifyData(fileData);

        WriteData(fileName, modifiedData);
    }
    catch (UnauthorizedAccessException ex)
    {
        // The caller does not have the required permission.
    }
    catch (PathTooLongException ex)
    {
        // The specified path, file name, or both exceed the system-defined maximum length.
        // For example, on Windows-based platforms, paths must be less than 248 characters
        // and file names must be less than 260 characters.
    }
    catch (ArgumentException ex)
    {
        // One or more paths are zero-length strings, contain only white space, or contain
        // one or more invalid characters as defined by InvalidPathChars.
        // Or path is prefixed with, or contains only a colon character (:).
    }
    catch (NotSupportedException ex)
    {
        // File name is in an invalid format.
        // E.g. path contains a colon character (:) that is not part of a drive label ("C:\").
    }
    catch (DirectoryNotFoundException ex)
    {
        // The path specified is invalid. For example, it is on an unmapped drive.
    }
    catch (FileNotFoundException ex)
    {
        // File was not found
    }
    catch (IOException ex)
    {
        // Various other IO errors, including network name is not known.
    }
    catch (Exception ex)
    {
        // Catch all for unknown/unexpected exceptions
    }
}

当记录并向用户显示错误消息时,我们希望尽可能描述出现错误以及可能导致解决的任何可能建议。如果文件被锁定,我们应该能够通知用户这样,以便他可以在文件发布后重试。

在上面的示例中,使用所有异常catch子句,我们仍然不知道哪个操作(上下文)导致异常。打开和读取文件,修改数据或将更改写回文件系统时是否发生异常?

一种方法是将try / catch块移动到每个“action”方法中。这意味着将相同的异常处理逻辑复制/重复到所有三种方法中。当然为了避免在多个方法中重复相同的逻辑,我们可以将异常处理封装到另一个常用方法中,该方法将调用捕获泛型System.Exception并将其传递。

另一种方法是添加“枚举”或其他定义上下文的方法,以便我们知道异常发生的位置如下:

public enum ActionPerformed
{
    Unknown,
    ReadData,
    ModifyData,
    WriteData,
    ...
}

private void ProcessFile(string fileName)
{
    ActionPerformed action;

    try
    {
        action = ActionPerformed.ReadData;
        string fileData = ReadData(fileName);

        action = ActionPerformed.ModifyData;
        string modifiedData = ModifyData(fileData);

        action = ActionPerformed.WriteData;
        WriteData(fileName, modifiedData);
    }
    catch (...)
    {
        ...
    }
}

现在,在每个 catch 子句中,我们将知道引发异常时正在执行的操作的上下文。

是否有建议的方法来解决识别与异常相关的上下文的问题?这个问题的答案可能是主观的,但如果有设计模式或推荐的方法,我想遵循它。

5 个答案:

答案 0 :(得分:1)

创建例外时,在投掷之前将其Message属性设置为描述性内容。然后在更高的位置,您可以向用户显示此消息。

答案 1 :(得分:1)

我们通常会记录异常(使用log4net或nlog),然后使用用户可以理解的友好消息抛出自定义异常。

答案 2 :(得分:1)

我的观点是MS本地化异常的消息属性的方法都是错误的。由于.NET框架有语言包,因此您可以从中文安装中获取神秘的(例如普通话)消息。作为一个不是已部署产品语言母语的开发人员,我该如何调试? 我将为面向技术开发人员的消息文本保留异常消息属性,并在其Data属性中添加用户消息。

应用程序的每一层都可以将用户消息从自己的角度添加到当前抛出的异常中。 如果您认为第一个异常确切地知道出错了什么以及如何修复它,您应该向用户显示第一个添加的用户消息。上面的所有架构层将具有较少的关于来自较低层的特定错误的上下文和知识。这将导致用户不太有用的错误消息。因此,最好在图层中创建用户消息,在该图层中您仍有足够的上下文,以便能够告诉用户出错的地方以及是否以及如何修复。

为了说明这一点,假设您有一个软件,其中包含登录表单,Web服务,后端和存储用户凭据的数据库。在检测到数据库问题时,您会在哪里创建用户消息?

  1. 登录表格
  2. 网络服务
  3. 后端
  4. 数据库访问层

    1. IResult res = WebService.LoginUser(“user”,“pwd”);
    2. IResult res = RemoteObject.LoginUser(“user”,“pwd”);
    3. string pwd = QueryPasswordForUser(“user”);
    4. 用户user = NHibernate.Session.Get(“user”); - > 抛出SQLException
  5. 数据库抛出SQLException,因为它处于维护模式。

    在这种情况下,后端(3)仍然有足够的上下文来处理数据库问题,但它也知道用户试图登录。

    UI将通过Web服务获取不同的异常对象,因为无法在AppDomain / Process边界保留类型标识。更深层次的原因是远程客户端没有安装NHibernate和SQL服务器,这使得无法通过序列化传输异常堆栈。

    您必须将异常堆栈转换为更通用的异常,这是Web服务数据协定的一部分,导致Web Service边界的信息丢失。

    如果您尝试使用最高级别的UI,请尝试将所有可能的系统错误映射到有意义的用户消息,将UI逻辑绑定到后端的内部工作。这不仅是一种不好的做法,也很难做到,因为您将缺少有用的用户消息所需的上下文。

     catch(SqlException ex)  
     {
        if( ex.ErrorCode == DB.IsInMaintananceMode ) 
           Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");
        ....
    

    由于Web服务边界,它实际上更像是

     catch(Exception ex)
     {
           Excepton first = GetFirstException(ex);
           RemoteExcepton rex = first as RemoteExcepton;
           if( rex.OriginalType == "SQLException" )
           {
               if( rex.Properties["Data"] == "DB.IsMaintainanceMode" )
               {
                  Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");
    

    由于异常将被其他层的其他异常包装,因此您在UI层中针对后端的内部进行编码。

    另一方面,如果您在后端层执行此操作,您就知道您的主机名是什么,您知道您尝试访问哪个数据库。当你在适当的水平上做事时,事情变得容易多了。

       catch(SQLException ex)
       {
           ex.Data["UserMessage"] = MapSqlErrorToString(ex.ErrorCode, CurrentHostName, Session.Database)'
           throw;
       }
    

    作为一般规则,您应该将用户消息添加到最深层中的例外,在该层中您仍然知道用户尝试执行的操作。

    此致,   Alois Kraus

答案 3 :(得分:0)

您可以创建自己的异常类,并将其从catch块中返回给用户,并将消息放入新的异常类中。

catch (NotSupportedException ex)
    {
        YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
        throw exception;
    }

您可以将任意数量的信息保存到异常类中,这样用户就可以获得所有信息,只有您希望他们拥有的信息。

编辑:

实际上,您可以在Custom Exception类中创建一个Exception成员并执行此操作。

catch (NotSupportedException ex)
{  
    YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
    exception.yourExceptionMemberofTypeException = ex;
    throw exception;
}

这样,您可以为用户提供一条好消息,同时也为他们提供潜在的内部异常。 .NET使用InnerException一直这样做。

答案 4 :(得分:0)

如果可能,您应该从每种方法中抛出不同的异常类型。例如,如果您担心.NET异常冲突,您的ModifyData方法可以在内部捕获共享异常类型并重新抛出它们。