How can I correctly inject `TempDataDictionary` into my classes?
Asked Answered
H

2

7

I have an app that is written using c# and ASP.NET MVC 5. I'm also using Unity.Mvc for dependency injection.

Along with many other classes, the class MessageManager is registered in the IoC container. However, the MessageManager class depends on an instance of TempDataDictionary to perform its work. This class is used to write temporary data for the views.

In order to resolve an instance of MessageManager I need to also register an instance of TempDataDictionary. I would need to be able to add values to TempDataDictionary from the MessageManager class and then I would need to access the temp data from the view. Therefore, I need to be able to access the same instance of TempDataDictionary in the views so I can write out the messages to the user.

Also, if the controller redirects the user elsewhere, I don't want to lose the message, I still want to be able to show the message on the next view.

I tried the following to register both TempDataDictionary and MessageManager:

Container.RegisterType<TempDataDictionary>(new PerThreadLifetimeManager())
         .RegisterType<IMessageManager, MessageManager>();

Then in my view, I have the following to resolve to an instance of IMessageManager

var manager = DependencyResolver.Current.GetService<IMessageManager>();

However, the message gets lost for some reason. That is, when I resolve manager, TempDataDictionary doesn't contain any messages that were added by MessageManager from the controller.

How can I correctly register an instance of the TempDataDictionary so the data persists until it is viewed?

UPDATED Here is my IMessageManager interface

public interface IMessageManager
{
    void AddSuccess(string message, int? dismissAfter = null);
    void AddError(string message, int? dismissAfter = null);
    void AddInfo(string message, int? dismissAfter = null);
    void AddWarning(string message, int? dismissAfter = null);
    Dictionary<string, IEnumerable<FlashMessage>> GetAlerts();
}
Hadst answered 30/4, 2018 at 20:1 Comment(4)
Can you share the interface of IMessageManager? As your question stands right now, I don´t really see the need of a wrapper around TempData when you could use it directly from your controllers and views.Imputation
@Imputation I updated my question with the interfaceHadst
This sound like a design issue. You will need to provide the MessageManager implementation as well. You appear to have a misunderstanding about how TempDataDictionary is used within the framework. Its sole purpose is to pass data between the current and next HTTP requests Check out this article to get a better understanding When to use ViewBag, ViewData, or TempData in ASP.NET MVC 3 applicationsAnnulation
@Annulation what are my options to be able to wrap the process of transferring flash messages from to the view?Hadst
I
6

The TempDataDictionary is an intrinsic part of your MessageManager implementation, therefore, you should implement it inside that class directly instead of registering it in the container.

Something like:

public class MessageManager : IMessageManager
{
    private TempDataDictionary _tempDataDictionary;

    [...]
}

However, IMHO, I don't think is a good practice to use TempDataDictionary out of a controller context so instead of implementing it in your class, you could pass it every time you add or retrieve a message:

void AddSuccess(IDictionary<string, object> tempData, string message);

You could also create a MessageManager instance for each request using PerThreadLifetimeManager and then you don't need to use the TempDataDictionary at all, you can just implement this yourself with regular lists or dictionaries:

public class MessageManager : IMessageManager
{
    private List<string> _successMessages = new List<string>();
    private List<string> _errorMessages = new List<string>();
    private List<string> _warningMessage = new List<string>();
    private List<string> _infoMessage = new List<string>();

    public void AddSuccess(string message)
    {
        _successMessages.Add(message);
    }

    public void AddError(string message)
    {
        _errorMessages.Add(message);
    }

    public void AddWarning(string message)
    {
        _warningMessages.Add(message);
    }

    public void AddInfo(string message)
    {
        _infoMessages.Add(message);
    }

    public List<string> SuccessMessages
    {
        get { return _successMessages; }
    }

    public List<string> ErrorMessages
    {
        get { return _errorMessages; }
    }

    public List<string> WarningMessages
    {
        get { return _warningMessages; }
    }

    public List<string> InfoMessages
    {
        get { return _infoMessages; }
    }
}

Then, register it per thread so everything is cleared out on each request:

Container.RegisterType.RegisterType<IMessageManager, MessageManager>
        (new PerThreadLifetimeManager());

A better aproach?

If you want to make sure that the list is kept until it has been read, even if it happens in another request, or if you are using async actions or ajax requests, you can create your own LifetimeManager implementation that resolves the instance of the above class per session, for example:

public class SessionLifetimeManager : LifetimeManager
{
    private string _key = Guid.NewGuid().ToString();
    public override void RemoveValue(ILifetimeContainer container = null)
    {
        HttpContext.Current.Session.Remove(_key);
    }
    public override void SetValue(object newValue, ILifetimeContainer container = null)
    {
        HttpContext.Current.Session[_key] = newValue;
    }
    public override object GetValue(ILifetimeContainer container = null)
    {
        return HttpContext.Current.Session[_key];
    }
    protected override LifetimeManager OnCreateLifetimeManager()
    {
        return new PerSessionLifetimeManager();
    }
}

Then replace PerThreadLifetimeManager by SessionLifetimeManager above and simply clear the list each time you access it, for example:

public List<string> InfoMessages
{
    get 
    { 
         // Some view has accessed the data, clear the list before returning
         var tempInfoMessages = new List<string>(_infoMessages);
         _infoMessages.Clear();
         return tempInfoMessages; 
    }
}

Reference:

The SessionLifetimeManager was borrowed from here: https://gist.github.com/CrestApps/a246530e386b95d0a05d36bb13805259

Imputation answered 2/5, 2018 at 22:54 Comment(7)
What your suggesting is great. The only issue is that PerThreadLifetimeManager may cause a problem if this class is used in async call. The thread will change and the data will not get transferred properlyHadst
And the last option doesn't work for you? Not using PerThreadLifetimeManager and just clearing the list when it's accessed?Imputation
If I understand your suggestion, removing PerThreadLifetimeManger would make it unity use TransientLifetimeManager which creates a new instance each time the class is resolved. So the list of messages will get cleared each time the class is resolved. I may be missing something in your answerHadst
You are right, sorry! I forgot Unity uses the TransientLifetimeManager by default, I updated my answer to specify that you should use ContainerControlledLifetimeManager so you have only one instance per application.Imputation
Yeah that would work only if change the list<string> to Dictionary<string<List<string>>> so each user have an entry otherwise there is no guarantee that a user will see his own messages. Do the key of the directory would be the username.Hadst
Argggh! Sorry again, I totally missed that. I updated the answer again. Hopefully the per-session manager will do the trick and will be cleaner than storing the user name (you may want to show error messages even if no user is connected).Imputation
The implementation of the life-time-manage seems obsolete. Here is an updated implementation which worked for me gist.github.com/CrestApps/a246530e386b95d0a05d36bb13805259Hadst
M
2

This isn't strictly an answer, but rather an alternative suggestion:

Also, if the controller redirects the user elsewhere, I don't want to lose the message, I still want to be able to show the message on the next view.

Just looking at the above suggests to me that storing your messages in the database might be a better option. This way, you can even go back in time and review old messages if you want.

// persist in EF db context
class Message {
    DateTime CreatedUtc { get; set; }
    DateTime SeenUtc { get; set; }
    string Text { get; set; }
    AspNetUser User { get; set; }
    // etc
}

You can then persist this message across requests, manage when to mark the message as seen, and even let the user see his old messages (if you want to do)

Mezereon answered 8/5, 2018 at 16:42 Comment(1)
Thanks for your suggestion. But, the idea is to store flash messages which isn't a good fit to store in a database. These messages are temporary and one time and for a one time useHadst

© 2022 - 2024 — McMap. All rights reserved.