Сохранить данные просмотра на RedirectToAction

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult CreateUser([Bind(Exclude = "Id")] User user)
{
        ...
        db.SubmitChanges();
        ViewData["info"] = "The account has been created.";
        return RedirectToAction("Index", "Admin");
}

Это не сохраняет текст "info" в viewdata после redirectToAction. Как мне обойти эту проблему самым элегантным способом?

Моя текущая идея - поместить материал из элемента управления Index в [NonAction] и вызвать этот метод как из действия Index, так и из действия CreateUser, но я чувствую, что должен быть лучший способ.

Благодарю.

7 ответов

Решение

Ты можешь использовать TempData,

TempData["info"] = "The account has been created.",

TempData существует именно для этой ситуации. Он использует Session в качестве хранилища, но его не будет после второго ответа.

Из MSDN:

Типичное использование объекта TempDataDictionary - для передачи данных из метода действия при его перенаправлении в другой метод действия. Например, метод действия может хранить информацию об ошибке в свойстве контроллера TempData (которое возвращает объект TempDataDictionary) до вызова метода RedirectToAction. Затем следующий метод действия может обработать ошибку и отобразить представление, отображающее сообщение об ошибке.

Использование ViewData если ваши данные должны быть доступны в View во время "этого" запроса. Используйте TempData, если ваши данные предназначены для "следующего" запроса (например, шаблон проектирования POST-REDIRECT-GET).

Если вам нужно это более одного раза, хорошим обходным путем будет создание ActionFilterAttributes, которые экспортируют / импортируют временные данные в viewdata и наоборот. Вы также можете очень хорошо передать свой ModelState таким образом (продемонстрировано здесь - #13). Я думаю, что с некоторыми изменениями в этом фрагменте кода у вас будет чистое решение.

Вы могли бы использовать TempData Свойство контроллера имеет тот недостаток, что оно использует хранилище сеансов в фоновом режиме. Это означает, что у вас будет дополнительная работа, чтобы заставить его работать в веб-ферме, и вам сначала нужно будет включить сеансы в вашем приложении.

Альтернативой является использование файлов cookie, если вам нужно всего лишь передать короткое сообщение. Это требует правильного шифрования куки. Не полагаясь на TempData Свойство также позволяет вам устанавливать сообщения не в контексте MVC, например, на классической странице ASHX.

Взгляните на FlashMessage, который может сэкономить вам часть работы, реализуя это самостоятельно.

Поскольку, как представляется, TempData использует хранилище, а любая форма ITempDataProvider, не являющаяся "внутрипроцессной", требует, чтобы объект был Serializable, TempData кажется крайне неадекватным в ситуациях веб-фермы... (ViewDataDictionary сам по себе не сериализуем...) У кого-нибудь есть предложения по этому поводу?

Я использую ActionFilters для передачи параметров запроса в ViewData и параметров метода контроллера в ViewData для returnUrl. Это было основано на https://andrewlock.net/using-an-iactionfilter-to-read-action-method-parameter-values-in-asp-net-core-mvc/

      // Here are the filters
namespace MyNamespace.Utilities.Filters {
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Primitives;
    using System.Linq;
    
    public class ReturnUrlQueryToViewDataActionFilter : IActionFilter {
    
        private readonly string parmName = "ReturnUrl"; // I actually keep this in a global static variable, but put here for example
    
        public void OnActionExecuting(ActionExecutingContext context) {         
            if (context.Controller is Controller controller && controller.Url is IUrlHelper urlHelper) {
                if (context.HttpContext.Request.Query[parmName] is StringValues stringValues/*Possible to have more than one matching query values(like a list) which would be comma seperated*/
                        && stringValues.Count == 1 /*returnurl itself should never be a list form, even though query parms object make it possible, so if more than 1, just ignore it completly*/
                        && stringValues.Single() is string returnUrl
                        && !string.IsNullOrWhiteSpace(returnUrl)) {
                    if (urlHelper.IsLocalUrl(returnUrl)) {
                        controller.ViewData[parmName] = returnUrl;                       
                    } else if (context.HttpContext.RequestServices.GetRequiredService<ILogger<ReturnUrlQueryToViewDataActionFilter>>() is ILogger logger) {// WAS NOT A LOCAL RETURN URL 
                        logger.LogWarning("RedirectUrl was NOT LOCAL URL: '{badReturnUrl}'; ViewData value WAS NOT SET.", returnUrl);
                    }
                }
            }
        }
        public void OnActionExecuted(ActionExecutedContext context) { }
    }
    // Used to take a "Named" parameter on the Action method
    public class ReturnUrlParameterToViewDataActionFilter : IActionFilter {
        private readonly string parmName = "ReturnUrl"; // I actually keep this in a global static variable, but put here for example
        public void OnActionExecuting(ActionExecutingContext context) {
            if (context.Controller is Controller controller && controller.Url is IUrlHelper urlHelper) {
                if (context.ActionArguments.TryGetValue(parmName, out object value)
                    && value is object obj
                    && obj != null
                    && obj.ToString() is string returnUrl
                    && !string.IsNullOrWhiteSpace(returnUrl)) {
                    if (urlHelper.IsLocalUrl(returnUrl)) {
                        controller.ViewData[parmName] = returnUrl;                      
                    } else if (context.HttpContext.RequestServices.GetRequiredService<ILogger<ReturnUrlQueryToViewDataActionFilter>>() is ILogger logger) {// WAS NOT A LOCAL RETURN URL 
                        logger.LogWarning("RedirectUrl was NOT LOCAL URL: '{badReturnUrl}'; ViewData value WAS NOT SET.", returnUrl);
                    }
                }
            }
        }

        public void OnActionExecuted(ActionExecutedContext context) { }
    }
}

// add to the DI Container
services.AddScoped<ReturnUrlQueryToViewDataActionFilter>();
services.AddScoped<ReturnUrlParameterToViewDataActionFilter>();


 // Example Usage of ReturnUrlQueryToViewDataActionFilter over a get method that may or maynot include a query parameter for "ReturnUrl", 
 [HttpGet]
[ServiceFilter(typeof(ReturnUrlQueryToViewDataActionFilter))]
public async Task<IActionResult> Edit(Guid? id, CancellationToken cancellationToken) {
    // do a get to db to get the model and return to view that will ultimatly do a "Post Action"
    // that uses a form with a hidden input for "ReturnUrl" that will then get picked up by the ReturnUrlParameterToViewDataActionFilter
}

// Example Usage of ReturnUrlParameterToViewDataActionFilter over a Post method that has a parameter on the Controller Action for "ReturnUrl"

[HttpPost]
[ValidateAntiForgeryToken]
[ServiceFilter(typeof(ReturnUrlParameterToViewDataActionFilter))]
public async Task<IActionResult> Edit(Guid id, string Name, string Description, string ReturnUrl){
    // value gets sent back to Edit View if reshown to user IE. ModelState was not valid
    
     // If Edit is successfull redirect the user (for safety check if Url is local, even though that is checked in the filters, can never be to safe     
     if(this.Url.IsLocal(ReturnUrl)){        
         // could be a few other things to check here for making sure url is correct depending 
         // on if you use virtual apps/paths, like checking for returnUrl.StartsWith('/') then prepend a ~ to get the virtual path to map correctly, but since this part can be very different depending on your setup      
         retun Redirect(this.Url.Content(ReturnUrl));        
     } 
}


// I also use this TagHelper to take the ViewData value and transform it into a hidden field
namespace MyNamespace.Utilities.TagHelpers {
    using Microsoft.AspNetCore.Razor.TagHelpers;
    public class ReturnUrlHiddenInputTagHelper : TagHelper {     
        private readonly string parmName = "ReturnUrl"; // I actually keep this in a global static variable, but put here for example   
        
        [ViewContext, HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; } // will be null until Process()   
        
        public override void Process(TagHelperContext context, TagHelperOutput output) {            
            string returnUrl = this.ViewContext?.ViewData?[parmName]?.ToString();
            if (string.IsNullOrWhiteSpace(returnUrl)) {// If no return url, dont display
                output.SuppressOutput();
                return;
            }
            output.TagName = "input";
            output.TagMode = TagMode.SelfClosing;
            output.Attributes.Add("type", "hidden");
            output.Attributes.Add("name", parmName);
            output.Attributes.Add("value", returnUrl);
        }
     
    }
}
// Then inside your _ViewImports.cshtml include whatever your base namespace is that holds the TagHelper
@addTagHelper *, MyNamespace


// Then inside the Edit.cshtml form you put, which will put a hidden field from the ViewData value if located, if not it does nothing
<return-url-hidden-input />

Ответ: TempData. Разница в использовании для уточнения заключается в следующем:

TempData = от действия к действию

ViewData = от действия к просмотру

Другие вопросы по тегам