4

我正在尝试找到一种方法来使用 Toastr 在我的应用程序中发生异常或错误时向用户显示错误。我遇到的问题似乎表明在控制器中发生异常,或者使用 Toastr 在当前视图中显示的数据访问层是不可能的。

我想知道你们中是否有人遇到过这种情况以及您的解决方案是什么?

我想要完成的是,只要有未处理的异常,或者有人手动处理异常,我们就有能力在不中断工作流程的情况下向用户显示错误。有人向我建议了 Toastr,但完全是 javascript,我不确定在我的 MVC4 应用程序中实现它的最佳方式。

我正在探索的一个选项是设置我的默认索引控制器来处理传入的错误字符串,以便我可以从 Global.asax.cs 中的 Application_Error 方法重定向到它,以便提供友好的重定向,然后如果传入字符串不为空,那么我可以在索引视图上使用 toastr。然而,这并不理想,因为它需要重定向,并且会破坏工作流程。此外,它不允许我在不引发异常或在 javascript 中进行所有错误处理的情况下显示错误。

其他重要信息是我们正在使用 Telerik Kendo UI 和 Razor 语法,如果这对我有任何帮助的话。

4

1 回答 1

9

对于那些和我有同样问题的人来说,解决方案是:

我在这里找到了我的解决方案的第一步:https ://github.com/martijnboland/MvcNotification

他实施了自己的通知形式。但我希望能够使用 Toastr,或任何其他类型的通知选项。

注意:任何地方你看到一个以“Res”结尾的类都是一个资源文件。这是为了让我们的应用程序中的字符串更有条理。这样就没有人会混淆了。

这是我实施解决方案的方式。注意:这也适用于 MVC5

首先要做的是在您的源代码中创建一个 Toastr 对象。这将用于最终在 UI 中向用户弹出消息。

public class Toast
    {
        public string type { get; set; }
        public string message { get; set; }
        public string title { get; set; }
        public string positionClass { get; set; }
        public int fadeIn { get; set; }
        public int fadeOut { get; set; }
        public int timeOut { get; set; }
        public int extendedTimeOut { get; set; }
        public bool debug { get; set; }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="message"></param>
        /// <param name="dtype"></param>
        public Toast(MessageType type, string message, DisplayType dtype = DisplayType.TopRight)
        {
            this.type = type.ToString();
            this.message = message;
            this.DType = dtype;
            this.fadeIn = 300;
            this.fadeOut = 1000;
            this.timeOut = 5000;
            this.extendedTimeOut = 1000;
            this.debug = false;
        }

        /// <summary>
        /// 
        /// </summary>
        public DisplayType DType
        { 
            set 
            { 
                this.positionClass = GetPositionClass(value); 
            } 
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="dtype"></param>
        /// <returns></returns>
        private string GetPositionClass(DisplayType dtype)
        {
            string position = string.Empty;

            switch (dtype)
            {
                case DisplayType.TopLeft:
                    position = ToastrProperties.TopLeft;
                    break;
                case DisplayType.TopFull:
                    position = ToastrProperties.TopFull;
                    break;
                case DisplayType.BottomRight:
                    position = ToastrProperties.BottomRight;
                    break;
                case DisplayType.BottomLeft:
                    position = ToastrProperties.BottomLeft;
                    break;
                case DisplayType.BottomFull:
                    position = ToastrProperties.BottomFull;
                    break;
                case DisplayType.TopRight:
                default:
                    position = ToastrProperties.TopRight;
                    break;
            };

            return position;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        public static List<Toast> DeserializeAll(string json)
        {
            return Newtonsoft.Json.JsonConvert.DeserializeObject<List<Toast>>(json);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="allToast"></param>
        /// <returns></returns>
        public static string SerializeAll(List<Toast> allToast)
        {
            return Newtonsoft.Json.JsonConvert.SerializeObject(allToast);
        }
    }

这使用了我为 Toastr 显示位置和消息窗口类型创建的两个特殊枚举,以便它们可以是动态的。

public enum MessageType
    {
        success,
        info,
        warning,
        error,
    };

public enum DisplayType
    {
        TopRight,
        TopLeft,
        TopFull,
        BottomRight,
        BottomLeft,
        BottomFull,
    };

创建 Toastr 类后,您必须覆盖控制器的 OnException 方法。如果您使用的是我还将展示的 ApiController,则必须以另一种方式发生这种情况。

您还需要创建一个 ToastrProperties 类,如下所示。

public static class ToastrProperties // TODO: Add in the descriptions for each of these properties
{
    /// <summary>
    /// 
    /// </summary>
    public const string MessagesKey = "messages";

    /// <summary>
    /// 
    /// </summary>
    public const string BottomFull = "toast-bottom-full-width";

    /// <summary>
    /// 
    /// </summary>
    public const string BottomLeft = "toast-bottom-left";

    /// <summary>
    /// 
    /// </summary>
    public const string BottomRight = "toast-bottom-right";

    /// <summary>
    /// 
    /// </summary>
    public const string TopFull = "toast-top-full-width";

    /// <summary>
    /// 
    /// </summary>
    public const string TopLeft = "toast-top-left";

    /// <summary>
    /// 
    /// </summary>
    public const string TopRight = "toast-top-right";

    /// <summary>
    /// 
    /// </summary>
}

控制器示例:

我建议为您的控制器创建一个特殊的基类,以便它们都继承自它,并且它可以帮助您稍后在应用程序中处理其他事情。这是我的基本控制器类。

    /// <summary>
    /// The Base Controller for the P3 Application. All Controllers that are not 
    /// API Controllers should derive from this
    /// </summary>
    public abstract class BaseController : Controller
    {

        // TODO: Preferably, new up through injection through constructor
        protected Services.P3KendoDataAccess Data = PortalServices.DataAccess;

        /// <summary>
        /// Handles any and all unhandled exceptions that occur
        /// within a standard MVC controller. This will Log the Error
        /// using NLog, and then display an error to he user using Toastr
        /// which will show that there was a problem within the controller
        /// </summary>
        /// <param name="filterContext"></param>
        protected override void OnException(ExceptionContext filterContext)
        {
            try
            {
                // Log the original error, and mark it as fixed so that the message isn't displayed to the User
                // TODO: Assign a GUID to the error, and display that to the user so that it can be referenced back to the exception
                P3Log.Error(filterContext.Exception, System.Web.HttpContext.Current);
                filterContext.ExceptionHandled = true;

                ((BaseController)filterContext.Controller).ShowMessage(new Toast(MessageType.error, filterContext.Exception.Message, DisplayType.TopRight), false);
            }
            catch (Exception excep)
            {
                P3Log.Error(new Exception(ToastrRes.BaseControllerException, excep));
            }

            return;
        }

    }

将它添加到项目后,只需将控制器设置为从此类而不是 Controller 派生,这将设置此方法。

WebAPI 控制器示例:

这一点涉及更多,因为您不能像上面的示例中那样仅从 ApiController 类继承。您必须创建一个将应用于每个 ApiController 的异常过滤器属性。我将向您展示如何在不手动应用的情况下做到这一点,因为您很可能希望在每个控制器上都使用它。

首先,您必须创建过滤器属性:

    public class P3ApiExceptionFilterAttribute : ExceptionFilterAttribute // TODO: Add information to the summaries
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="Context"></param>
        public override void OnException(HttpActionExecutedContext Context)
        {
            try
            {
                List<Toast> Toasts = new List<Toast>();

                // Create a response and add a header for the Message to be displayed using the ajaxError event
                Context.Response = Context.Request.CreateResponse();

                // Log the error that occurred here
                P3Log.Error(Context.Exception);

                // Go through all of the Headers that match our messages key. There should only ever be
                // one, but since the Web API stuff handles this differently I want to cover our bases
                foreach (var header in Context.Request.Headers.Where(x => x.Key.Equals(ToastrProperties.MessagesKey)))
                {
                    // Check the header to see if it's null, and if it's not, and there are values for
                    // the header, add them to the Toasts list so that they will be re-added to the error
                    // response header, and actually be received by the client
                    if (header.Value != null)
                    {
                        foreach (string str in header.Value)
                        {
                            if (!string.IsNullOrEmpty(str))
                            {
                                try
                                {
                                    Toasts.AddRange(Toast.DeserializeAll(str));
                                }
                                catch { } // Do nothing here
                            }
                        }
                    }

                }

                // Add the Exception Toast
                Toasts.Add(new Toast(MessageType.error, GlobalRes.ApplicationError, DisplayType.TopRight));

                // Add the header for the response so that the messages will be displayed
                // once the response gets back to the client
                if (Toasts != null && Toasts.Any())
                {
                    string Messages = Toast.SerializeAll(Toasts);

                    if (!string.IsNullOrEmpty(Messages))
                    {
                        // Adding a single Response Header
                        Context.Response.Headers.Add(ToastrProperties.MessagesKey, Messages);
                    }
                }
            }
            catch (Exception excep)
            {
                P3Log.Error(ToastrRes.ApiToastrException, excep);
            }

            base.OnException(Context);
        }
    }

接下来,您需要将过滤器属性添加到所有 Api 控制器。最简单的方法是进入您的“WebApiConfig.cs”文件,然后在 Register 方法中输入:

            // Add the exception handler for the API controllers
            config.Filters.Add(new P3ApiExceptionFilterAttribute());

这将设置您的 WebApi 控制器。

下一步

在添加了其中一种/两种方法之后,您需要做一些其他的事情。

首先在我们开始之前,重要的是要让您知道我们在这两种方法中所做的实际上是处理错误,并将它们记录在我们的系统中。然后我们使用 Toast 对象静态方法将 JSON 序列化和反序列化为请求的 response/temp 标头,然后将其作为 JSON 传递回客户端,并且可以在异步或回发页面请求时由浏览器处理. 但我们将在一秒钟内完成。

因为我不希望它仅用于将异常消息传递给客户端,所以我还为 BaseController 和 ApiController 方法设置了扩展,以便它们可以调用“ShowMessage”方法并将 Toastr 方法发送到客户端。

这是扩展的基本控制器版本:

public static class ControllerExtensions
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="toast"></param>
        /// <param name="showAfterRedirect"></param>
        public static void ShowMessage(this Controller controller, Toast toast, bool showAfterRedirect = false)
        {
            try
            {
                if (toast != null)
                {
                    List<Toast> allToast = new List<Toast>();

                    // Pull the existing messages from the Temp, or Response 
                    // based on the redirect option, and assign it to a string variable
                    string messagesJson = showAfterRedirect ?
                        controller.TempData[ToastrProperties.MessagesKey].ToString()
                        : controller.Response.Headers[ToastrProperties.MessagesKey];

                    // Deserialize the JSON into the toast list
                    if (!string.IsNullOrEmpty(messagesJson))
                    {
                        try
                        {
                            allToast = Toast.DeserializeAll(messagesJson as string);
                        }
                        catch { } // Do nothing here
                    }

                    // Add a new Toast to the list
                    allToast.Add(toast);

                    // Serialize the List
                    string SerializedString = Toast.SerializeAll(allToast);

                    if (!string.IsNullOrEmpty(SerializedString))
                    {
                        if (showAfterRedirect)
                        {
                            controller.TempData[ToastrProperties.MessagesKey] = SerializedString;
                        }
                        else
                        {
                            controller.Response.Headers[ToastrProperties.MessagesKey] = SerializedString;
                        }
                    }
                }
            }
            catch (Exception excep)
            {
                P3Log.Error(new Exception(ToastrRes.ShowMessageException, excep));
            }
        }
    }

这是同一扩展的 Web Api 版本:

public static class ApiControllerExtensions
    {
        /// <summary>
        /// Show a message to the user Using Toastr
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="messageType"></param>
        /// <param name="message"></param>
        public static void ShowMessage(this ApiController controller, Toast ToastMessage)
        {
            try
            {
                string message = string.Empty;

                List<Toast> Messages = new List<Toast>();

                var header = controller.Request.Headers.FirstOrDefault(x => x.Key.Equals(ToastrProperties.MessagesKey));

                if (header.Value != null && header.Value.Any())
                {
                    string hString = header.Value.FirstOrDefault();

                    if (!string.IsNullOrEmpty(hString))
                    {
                        try
                        {
                            Messages = Toast.DeserializeAll(hString);
                        }
                        catch {} // Do nothing here
                    }
                }

                // Add the message to the existing messages in the
                // header
                Messages.Add(ToastMessage);

                message = Toast.SerializeAll(Messages);

                if (!string.IsNullOrEmpty(message))
                {
                    // Remove the old header, and put the new one in
                    controller.Request.Headers.Remove(ToastrProperties.MessagesKey);

                    controller.Request.Headers.Add(ToastrProperties.MessagesKey, message);
                }
            }
            catch (Exception excep)
            {
                // Log here with NLog
                P3Log.Error(new Exception(ToastrRes.ShowMessageException, excep));
            }
        }
    }

像任何标准扩展一样,您需要确保包含命名空间,否则它将无法工作。

最后一步:

安装 Toastr NUGET 包,或使其在线,并确保它已添加到您的包中,或者您用于将脚本添加到视图中的方法。

现在您需要将 Javascript 添加到应用程序中的 _Layout.cshtml。

<script type="text/javascript">

        // Setup message triggers and display all messages for this page
        $(document).ready(function () {
            var tempMessages = '@Html.Raw(TempData[ToastrProperties.MessagesKey])';

            if (!tempMessages) {
                tempMessages = '[]';
            }

            var viewMessages = '@Html.Raw(Response.Headers[ToastrProperties.MessagesKey])';

            if (!viewMessages) {
                viewMessages = '[]';
            }

            var allMessages = $.parseJSON(tempMessages).concat($.parseJSON(viewMessages));

            handleAjaxMessages();

            displayMessages(allMessages);
        });

        // Display all messages that are listed within the Header of the call.
        // These messages are all stored in a serialized XML string that is then Decoded by the RenderMessages method
            function displayMessages(messages) {
                    $.each(messages, function (idx, msg) {
                            toastr[msg.type](msg.message, msg.title, {
                                    fadeIn: msg.fadeIn,
                                    fadeOut: msg.fadeOut,
                                    timeOut: msg.timeOut,
                                    positionClass: msg.positionClass,
                                    onclick: function() {
                                            var wnd = $("#AppMessageWindow").data("kendoWindow");
                                            wnd.content(msg.message).center().open();
                }
                            });
                    });
            }

        // Add methods for events that are both ajaxSuccess, and ajaxError
        function handleAjaxMessages() {
            $(document).ajaxSuccess(function (event, request) {
                checkAndHandleMessageFromHeader(request);
            }).ajaxError(function (event, request) {
                checkAndHandleMessageFromHeader(request);
            });
        }

        // Get messages from the Response header of the request, and display them as
        // a message using Toastr
        function checkAndHandleMessageFromHeader(request) {
            // pull the messages from the Response Header
            var msgs = request.getResponseHeader('@ToastrProperties.MessagesKey');

            if (!msgs) {
                msgs = '[]'
            }

            var allMessages = $.parseJSON(msgs)

            displayMessages(allMessages);
        }

    </script>

这需要一些解释。脚本中的第一个函数加载初始响应/临时标头,因为在初始页面加载时,页面内没有触发标准请求。或者至少我找不到允许访问标题的。所以这些都放在使用 Razor 中。

其余的应该很简单。它使用 JSON 弹出 toastr 消息,并向 Ajax 请求添加事件,以便正确处理返回给它的任何 Toastr 消息。

我很确定我已经得到了这里的一切。如果您有任何问题,或者在尝试实施时缺少某些内容,请在此处发帖或 PM 我,我会更新我的帖子。我希望这可以帮助其他尝试做同样事情的人。:)

享受!

于 2013-10-21T20:50:58.367 回答