-
Notifications
You must be signed in to change notification settings - Fork 218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SiteMapTitle] Action Filter Attribute Seems to be broken #236
Comments
The SiteMapTitleAttribute doesn't read from the ViewBag object, only from a custom ViewModel or Model object. To make this work, you will need a Model. [SiteMapTitle("Title")]
public ActionResult Index()
{
var model = new HomeModel { Title = "test" };
ViewBag.Title = model.Title;
return View(model);
}
public class HomeModel
{
public string Title { get; set; }
} See the following documents for other examples: |
Hi, [SiteMapTitle("Title")] //tried [SiteMapTitle("PageDetails.PageTitle")] & [SiteMapTitle("SiteMapTitleAttr")] public ActionResult Index() { var vm = VMProvider.GetPageDetailsVM(); ViewBag.Title = vm.PageDetails.PageTitle; //for html title ViewData["SiteMapTitleAttr"] = vm.PageDetails.PageTitle; //page title needs to be displayed in SitemapTitle Helper & SiteMapPath Helper return View("PageDetails", vm); } i am using this for display title in @Html.MvcSiteMap().SiteMapTitle() @Html.MvcSiteMap().SiteMapPath() Html title i am getting the value but page title and breadcrumb i am getting only the xml attribute. Thanks. |
Hi, Following is the stack trace Object reference not set to an instance of an object. Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. Exception Details: System.NullReferenceException: Object reference not set to an instance of an object. Source Error: An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below. Stack Trace: [NullReferenceException: Object reference not set to an instance of an object.] MvcSiteMapProvider.Web.Mvc.Filters.SiteMapTitleAttribute.OnActionExecuted(ActionExecutedContext filterContext) +413 System.Web.Mvc.Async.<>c__DisplayClass4f.b__49() +111 System.Web.Mvc.Async.<>c__DisplayClass4f.b__49() +920551 System.Web.Mvc.Async.<>c__DisplayClass4f.b__49() +920551 System.Web.Mvc.Async.<>c__DisplayClass37.b__36(IAsyncResult asyncResult) +15 System.Web.Mvc.Async.<>c__DisplayClass2a.b__20() +33 System.Web.Mvc.Async.<>c__DisplayClass25.b__22(IAsyncResult asyncResult) +921356 System.Web.Mvc.<>c__DisplayClass1d.b__18(IAsyncResult asyncResult) +28 System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +20 System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +67 System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +20 System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +53 System.Web.Mvc.<>c__DisplayClass8.b__3(IAsyncResult asyncResult) +42 System.Web.Mvc.Async.<>c__DisplayClass4.b__3(IAsyncResult ar) +20 System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +53 System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +453 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +371 |
I created a simple demo project so I could verify it works (and it does): https://github.com/NightOwl888/MvcSiteMapProvider_236 BTW - the information I gave you previously was not entirely accurate - you don't have to create a model, you can also use the ViewData property. Of course, the title will only be overridden for the specific request that calls your action method - in all other cases it will show what you have configured for Title on the SiteMapNode. SiteMapTitle is generally only useful if you are using preservedRouteParameters to force several requests to match a specific node (such as when you are using CRUD operations to update data). In this case you would usually want your node title to be different for each database record even though the node is always the same. See the How to make MvcSiteMapProvider Remember a User's Position (and the included download) for a complete example of how this works. As for the exception, please create a small demo project exhibiting the behavior and either post it on GitHub or zip it and make it available for download so I can try to determine what is happening and, if necessary, create a patch. |
Hi, <add key="MvcSiteMapProvider_CacheDuration" value="0" /> the text "this is the title" changed to about. Thanks. |
i can also reproduce the Object reference not set to an instance of an object issue in your solution when cache set to 0; |
Hi, Thanks much!!! |
I am unable to reproduce the null reference exception (using the demo project I created, refreshing in rapid succession on the about page), but I have confirmed that the SiteMapTitle attribute doesn't work in this case. Just out of curiosity, why are you setting CacheDuration to 0? I am not entirely sure that setting a zero cache timeout is a supported configuration for System.Runtime.Caching.MemoryCache, and that might be why you are getting an exception. If you need to disable caching, it would be far better to inject an ICacheProvider implementation that follows the null object pattern - that is, a cache provider that doesn't store or retrieve anything. |
Hi, |
I now understand why your title was getting lost with CacheDuration set to 0. Basically, without a cache every call to the SiteMap would result in a new instance of the SiteMap being built. So setting the title in one line and reading it back in the next line would not work, because you would actually be getting a different instance in the second line. <mvcSiteMapNode title="The default title"/> MvcSiteMapProvider.SiteMaps.Current.CurrentNode.Title = "My Title";
var title = MvcSiteMapProvider.SiteMaps.Current.CurrentNode.Title;
// Result of title: The default title This would normally not be a problem, because when overriding the title this way it is being written to the request cache anyway. However, as an extra precaution to ensure that a different instance with the exact same values doesn't accidentally read back the request cache of the current node, an instance ID (random GUID) is added to the request cache key that can only be read back by the same object instance. For MvcSiteMapProvider to function correctly without a cache, it would need the SiteMap object to be a request cached so for the scope of an individual request all of the code would be accessing the same SiteMap instance and therefore refer to the same values. Otherwise the SiteMap would be rebuilt several times during the request depending on how many HTML helpers are defined in the markup and how many action filter attributes are defined, etc. This is a bug. But it is a bug that would be nearly impossible to encounter when using caching - you would literally have to start the request just before the timeout of the cache, write some values into the related request cache before the expiration and read them back after the cache has been rebuilt. That said, it is still something that should be fixed since there is value in disabling caching in the case where you are using ISiteMapNodeProvider or IDynamicNodeProvider and it is requesting data from a source that is already cached. So when I get a chance, I will add request caching of the SiteMap (in the SiteMapLoader) and the ability to actually disable caching via configuration (using a NullCacheProvider). |
I managed to work out what is causing the exception - it is a timing issue. When the cache times out, it immediately calls the This issue only happens when cache expires in the middle of a request. Setting the cache timeout to 0 makes the issue much more likely to happen, but requires the request to last at least a few milliseconds in order to get the exception. Anyway, I worked out how this can be solved, and I have a working prototype. It consists of the following.
The only problem is that the reference counting must always be decremented when the request is finished with it. My prototype uses a global action filter attribute to dereference the request, but it fails if there is an exception. This means there will be a resource leak if any unhandled exception occurs in any request because the reference count will never reach 0 in that case. So, there are really only 2 other options (that I know of) than using an MVC action filter attribute:
While implementing an HTTP module would be easier to upgrade to, HTTP modules are particularly unfriendly for DI because they require a public constructor to function. Not to mention, creating one just to keep track of when it is safe to dispose something feels a bit hacky. There is also the risk that it might not play nice with other HTTP modules. On the other hand, changing the SiteMap object so there will be a memory leak if it is not used in a using block will cause memory leaks for anyone who doesn't change their code to call it properly. That said, this is the same for using entity framework context - there is a resource leak unless you ensure it is disposed properly. In addition, the amount of code out there that calls the SiteMap object through use of the static method is likely to be small by comparison to those who are using the HTML helpers. Despite the bigger risk of accidentally causing a memory leak, the fact that this method is commonplace in the .NET framework makes me think this is a better way to go. If we go with the second option, I guess the question is - is this too big of a change for a minor release? I mean, it will require everyone who is accessing the MvcSiteMapProvider.SiteMaps.Current or MvcSiteMapProvider.SiteMaps.GetSiteMap() methods to put them in a using block like this: using (var siteMap = MvcSiteMapProvider.SiteMaps.Current)
{
var currentNode = siteMap.CurrentNode;
// Do something with the node here
} If it is too big of a change for 4.x, then I guess another option would be to slide in an HTTP module now, and switch to the using block syntax for 5.x. The thing is, any existing code will still function, it will simply cause a memory leak if it is not changed. This seems to be a "backward compatible" change, although a warning or two about this in the documentation would definitely be in order. Thoughts? |
To be honest, I would vouch for the IDisposable option. Reference counting should work too, but it has more potential issues (e.g. never refreshing sitemaps because a reference isn't cleared, failure to load the IHttpModule because of server config and so on. The IDisposable pattern seems good to work with: it creates a temporary lock for those who need it but doesn't interfere with other workflows we currently support. |
Actually, the reference counting would still be required because the SiteMap is a shared object. The primary difference between the 2 methods is that the Dispose() method would be decrementing the reference counter rather than relying on an outside component (HTTP module) to do it. Putting it in a using block ensures the reference count will always be decremented no matter what the outcome. However, skipping the call to Dispose() even once will cause the count to be off and the SiteMap (which has circular object references) will never get GC'd. We still need to wait until all requests are done with the SiteMap before the Clear() method can safely be called, so (unless I have missed another option) the reference counter is essential. However, since doing it that way means that we won't necessarily have the same SiteMap instance from one using block to the next (even in the same request), the request cache keys need to be adjusted so they contain the SiteMapCacheKey (and in the case of nodes, the Key of the node) instead of an instance id - this will ensure the ID won't change from one object instance to the next and the request cached values will still be accessible. |
My initial idea was to go with a clone-per-request but that would bump the memory footprint required quite a lot. I guess the reference counting is the only way to go, no? On highly loaded sites, this would introduce a chance a sitemap never gets GC'd? |
After the reference counting is in place, the amount of traffic shouldn't matter anymore. In our current version, a highly loaded site with a short cache timeout will increase the chance of a null reference exception to the point where it is almost certain. It would work something like this:
The only chance of a memory leak is the case where the using block is not used, and then it is certain to happen. The problem is - any code out there that is accessing the SiteMap statically is certainly not fenced in a using block because it is not currently supported. So, that is why I am hesitant to throw it out there in version 4.x - this is a major API change for those (likely few) developers who are using it. Any usage of the HTML helpers would be covered because we can easily change the calls to the static SiteMaps.Current method to the using block. On the other hand, using a HTTP module doesn't change the API so it would not require any code changes. I suppose it could be registered programmatically, but I think even that will require an HTTP module to be registered in the web.config file to fire it off (unless it will work with WebActivator's PreApplicationStart method). Still, when using an HTTP module, it will likely mean switching to a strategy that uses a service locator for DI otherwise the SiteMapLoader, SiteMapSpooler, etc. won't be able to be swapped out. I'd really prefer not to go down that road even if it means an API change that could potentially cause a memory leak. So, I am torn between
|
Let's go with 3 then? We can do the MVC5 support in v5 as well. Gives us some time to test. |
Agreed. 3 seems like the best choice, but means it might be a while before the exceptions are fixed. But then, nobody is complaining about them - most likely because when they are encountered they are impossible to duplicate. There are some other design issues that I have been keeping track of in my head (that I should have written down) that will need to be addressed in v5. For now, I will document them here, but eventually they will need to be made into their own separate issues (perhaps labeled with a v5 label to keep them separate). I will also keep adding to this list as I think of them so they are listed in one place.
Many of these items depend on the fact that the key logic needs to change, which makes them ideal to implement together instead of as one-offs. v5 is clearly going to need a serious commitment of time, which is something I can't provide right now. I don't think MVC5 support can wait for all of these to get completed and facilitating it should be an additive change that doesn't affect backward compatibility of the current API. I also think that item 4 and 5 above can probably be done in v4 so at least the original problem described in this issue can be addressed now without changing the API too much (only the constructor of the SiteMap will be changed, which isn't likely an object many people have subclassed or implemented themselves). |
…tribute doesn't work when setting the CacheDuration to 0. The other issue in maartenba#236 (random exceptions) is still open.
…che expires. Assumption about a potential memory leak because of circular references was incorrect, so there is no need for the SiteMapLoader to call the ISiteMap.Clear() method when the cache expires. Confirmed by adding extra buffer to the SiteMapNode and setting cache duration to 0 that memory is not leaking in this case.
Both issues:
Have been addressed, so I am marking this closed. |
Hi,
[SiteMapTitle] Action Filter Attribute Seems to be broken. Not updating the values from the action result. Only taking the xml title. MvcSiteMapProvider_CacheDuration set to 0.
Tested with 4.3.0 & 4.4.4.
Thanks.
The text was updated successfully, but these errors were encountered: