使用MVC4和Entity Framework在Toastr或类似方法中显示错误和异常

时间:2013-08-27 19:38:31

标签: javascript asp.net-mvc-4 logging error-handling toastr

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

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

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

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

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

1 个答案:

答案 0 :(得分: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类,就必须覆盖Controller的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序列化和反序列化为请求的响应/临时标头,然后将其作为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我,我会更新我的帖子。我希望这可以帮助那些试图做同样事情的人。 :)

享受!