Displaying Errors and Exceptions in Toastr, or sim

2019-03-30 12:33发布

问题:

I'm trying to find a way to use Toastr to display errors to users as Exceptions or Errors occur within my application. The problems I'm running into seem to suggest that it would not be possible to have an exception that occurs in the Controller, or Data Access layer displayed in the current view using Toastr.

I'm wondering if any of you have run into this scenario and what your solution to it was?

What I'm trying to accomplish is that any time there is an unhandled exception, or someone handles an exception manually that we have the ability to display the error to the user without disrupting workflow. Toastr was suggested to me, but being completely javascript I'm not sure the best way to implement it within my MVC4 application.

One option I'm exploring is setting up my default index controller to handle an incoming error string so that I can redirect to it from the Application_Error method in the Global.asax.cs in order to give a friendly redirect, and then if that incoming string is not null then I can use toastr on the Index view. However this is not ideal because it requires a redirect, and disrupts workflow. Also it will not allow for me to display an error without having to thrown an exception or do all my error handling within the javascript.

Other important information is that we are using Telerik Kendo UI, and Razor Syntax if that would help me in any way.

回答1:

For those of you who have this same question that I had here is the solution:

I found the first step of my solution here: https://github.com/martijnboland/MvcNotification

He had implemented his own form of Notification. But I wanted to be able to use Toastr, or any other kind of Notification options that were out there.

NOTE: Anywhere you see a class that ends in "Res" it's a resource file. This is to keep our strings in our application more organized. That way nobody gets mixed up with that.

Here is how I implemented my solution. NOTE: This works with MVC5 as well

First thing to do is the create a Toastr object in your source code. This will be used to pop the message to the user in the UI eventually.

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);
        }
    }

This uses two special enumerations I created for the Toastr display locations, and message window types so that they could be dynamic.

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

And

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

Once you've created the Toastr class you have to override the OnException method of your Controller. There is another way this has to happen if you are using an ApiController which I will also show.

Also you will need to create a ToastrProperties class, seen below.

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>
}

Controller Example:

I suggest creating a special base class for your controllers so that they all inherit from it, and it can help with other things later in your application. Here is my base controller class.

    /// <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;
        }

    }

After you've added this to your project just set your controllers to derive from this class instead of Controller, and that will set this method up.

WebAPI Controller Example:

This one is a little more involved because you can't just inherit from the ApiController class like in the above example. You have to create an Exception Filter Attribute that you would apply to each ApiController. I will show you how you can do it without manually applying it since you will want it on every controller anyways most likely.

First you have to create the Filter Attribute:

    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);
        }
    }

Next you need to add your Filter Attribute to all of your Api Controllers. The easiest way to do this is to go into your "WebApiConfig.cs" file, and inside of the Register method just put:

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

This will setup your WebApi Controllers.

NEXT Step

After you've added either/both methods you need to do a few other things.

First before we go into that though it's important to let you know that what we are doing here in these two methods are actually handling the errors, and logging them within our system. Then we are using the Toast objects static methods to serialize and deserialize JSON into the response/ temp headers of the request so that it's then passed back to the client as JSON and can be handled by the browser upon both async, or post back page requests. But we will get to that in a second.

Because I didn't want this to only be used for passing exception messages to the client I also setup extensions for both the BaseController, and ApiController methods so that they could call a "ShowMessage" method and send Toastr methods down to the client.

Here is the Base Controller version of the Extension:

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));
            }
        }
    }

Here is the Web Api version of the same extension:

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));
            }
        }
    }

Like any standard extension you need to make sure to have the namespace included otherwise it won't work.

Final Step:

Install the Toastr NUGET Package, or get it online, and make sure that it's added to your bundles, or the method you are using to add scripts to your Views.

Now you need to add the Javascript to the _Layout.cshtml in your application.

<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>

This requires some explanation. The first function in the script loads the initial response / temp headers because on the initial page load there isn't a standard request that is triggered within the page. Or at least I couldn't find one that would allow access to the headers. So these are placed in using Razor.

The rest should be pretty straight forward. It uses the JSON to pop a toastr message, and adds events to the Ajax requests so that any Toastr messages that come back to it are handled properly.

I'm pretty sure I've got everything in here. If you have any questions, or something is missing when you try to implement it, post on here or PM me and I'll update my post. I hope this helps others who are attempting to do the same thing. :)

Enjoy!