A couple of weeks ago I posted some advice on the DotNetNuke forum regarding the use of the 'NavigateURL' call. Through my work on Friendly Url Providers I've become a bit of a walking reference for this particular corner of the DNN framework. So I posted a short description on all the different overloads for the NavigateUrl call.
In it, I posted the cheerful snippet:
"[Same as the other NavigateUrl overloads] but allows you to pass in your own portalSettings instance. This allows you to define different portal options, so you can either change the current language, change the portal alias, or even generate urls for a different portal altogether. All overloads without 'portalSettings' produce Urls for the current portal, with all the current settings like the current language, current portal alias and more. You would also supply the 'portalSettings' value for use when the portalSettings is not in the current context, such as in DotNetNuke scheduled events - the current portal settings is only created and stored in the context.items when the request is as a result of a 'user' request to the website. Scheduled Items can be triggered by a timer and as such don't always have the 'portalSettings' instanced. An example would be generating links for an automated email created in a scheduled task."
The reason I posted this much information specifically about scheduled tasks, is that I've built a few now, and I know that using the DotNetNuke framework in 'scheduled task' mode can be a bit tricky. The reason is that when a scheduled task is kicked off, it's done in a different application thread to the main asp.net application, and as such, many of the Http items that the DNN framework takes for granted are missing. There's just one problem with this detailed advice, as we will find out, it's total hogwash and doesn't work.
What really happens when you provide your own PortalSettings
In the last day or so, I've been working on a DNN schedule item that does exactly what I posted in my forum response : generates an email to send to users, and in that email is links to pages within the site. Of course, as any good DNN developer knows, to get Page Urls, you use the DotNetNuke.Common.Globals.NavigateUrl() call. And, forgetting my own advice, the first thing I tripped up on was a 'object reference not set' error. A quick trace, a short slap to the forehead - of course! No Context.Items["PortalSettings"] instance, because there's no HttpContext.Current instance in a DNN schedule item thread. Incidentally, the check in the HttpContext for the PortalSettings call is this method, in 'PortalController.vb'
Public Shared Function GetCurrentPortalSettings() As PortalSettings
Return CType(HttpContext.Current.Items("PortalSettings"), PortalSettings)
End Function
Note that this method doesn't first check to see if HttpContext.Current is a valid reference - it's just assumed that it will be.
So, I dutifully took my own advice and came up with a new PortalSettings instance- easy peasy, I thought, I'll just grab the first portal alias in the list, and create a new portal settings object like this:
PortalSettings ps = new PortalSettings(-1, alias.HttpAlias);
Whoops! That doesn't work either. Why? Because, believe it or not, one of the things in the Portal Settings constructor actually seems to check the Current Portal Settings. This wouldn't be so bad (as it copes with 'null' as a return value), except that it assumes that the HttpContext.Current will never be null.
Back to Square One with the Portal Settings Constructor
So I sat down and wrote a whole procedure which replicated the constructor for the portal settings object, only in C# to match the project I was working on. This worked OK, after I removed the code that expected the HttpContext.Current to be a valid value. Armed with my new PortalSettings instance, I then called the NavigateUrl() call overload that allows you to pass in your own PortalSettings instance. I sat back, ran my code and waited for the output of a set of perfectly formed Urls.
I didn't have to wait long, as I got the long and wordy stream of Exception text back instead. Seems as though, even if you supply the PortalSettings object into the NavigateUrl overload, one of the first things it does is check the 'GetEnabledLocales' method, and the first line in that call is ... you guessed it 'PortalController.GetCurrentPortalSettings()' - which, as shown above, checks the HttpContext for the current portal settings. Now this is a bit of a silly situation - providing an overload to supply your own PortalSettings instance, and then checking the current Context for a PortalSettings instance. I'll probably log this one in Gemini to fix.
Interesting Sidebar to this Discussion
I often get people reporting 'object not set' reference errors when using various DNN modules and the Url Master / iFinity Friendly Url Provider modules. Normally, this shows up in a stack trace as eminating from the 'GetEnabledLocales' procedure, like this:
System.NullReferenceException: Object reference not set to an instance of an object. at DotNetNuke.Entities.Portals.PortalController.GetCurrentPortalSettings()
The reason for this is that it is the DotNetNuke UrlRewriter that is responsible for adding the current PortalSettings object into the HttpContext.Current.Items collection. The Url Master / Friendly Url Provider modules have a set of filters designed to stop requests at different points of processing. The 'ignoreRegex' filter means the UrlRewriter ignores the request completely, while the 'doNotRewriteRegex' ensures that the request is not rewritten, but still allows the PortalSettings to be created and added to the context. So if you've got this error, what you want to do is craft your 'ignoreRegex' to let the request through, but the 'doNotRewriteRegex' to block it. In between these two filters is the code to identify the portal and store the PortalSettings object in the HttpContext.
So, getting back to my project... what to do now? I still need to call 'NavigateUrl' without a HttpContext. I decided to go back to Square Zero and mock up the HttpContext completely, the same as I have done in my series of unit testing DotNetNuke modules.
Forward to Square Two with the PortalSettings Constructor
(I've always assumed it's square two that you move on to from square one, not circle one - some sort of integer over shape preference, I presume)
To fix the missing HttpContext problem without butchering up the DNN Core with custom fixes, I simply added a butchered up mock HttpContext constructor, in which I would stash my created portal settings. This creates a fake HttpContext object, stores the PortalSettings in the items collection, and the rest of the DNN Framework appears none the wiser. My Schedule Program now generates Emails with the correct Urls embedded, and I've got a bit of custom portal settings code to use next time I head up this pathway.
Here's the code. Don't use it unless you know what you're using it for, and if you're working on a high load server, probably best to make sure the threads are being disposed of correctly. Please don't ask me for a VB version either : go and find a C#/VB.NET Converter. The code is taken from the DNN Core, so all the usual legal messages in the DNN code files applies.
public static DotNetNuke.Entities.Portals.PortalSettings CreateNewPortalSettings(int portalId)
{
//new settings object
PortalSettings ps = new PortalSettings();
//controller instances
PortalController pc = new PortalController();
TabController tc = new TabController();
PortalAliasController pac = new PortalAliasController();
//get the first portal alias found to be used as the current portal alias
PortalAliasInfo portalAlias = null;
PortalAliasCollection aliases = pac.GetPortalAliasByPortalID(portalId);
string aliasKey = "";
if (aliases != null && aliases.Count > 0)
{
//get the first portal alias in the list and use that
foreach (string key in aliases.Keys)
{
aliasKey = key;
portalAlias = aliases[key];
break;
}
}
//get the portal and copy across the settings
PortalInfo portal = pc.GetPortal(portalId);
if (portal != null)
{
ps.PortalAlias = portalAlias;
ps.PortalId = portal.PortalID;
ps.PortalName = portal.PortalName;
ps.LogoFile = portal.LogoFile;
ps.FooterText = portal.FooterText;
ps.ExpiryDate = portal.ExpiryDate;
ps.UserRegistration = portal.UserRegistration;
ps.BannerAdvertising = portal.BannerAdvertising;
ps.Currency = portal.Currency;
ps.AdministratorId = portal.AdministratorId;
ps.Email = portal.Email;
ps.HostFee = portal.HostFee;
ps.HostSpace = portal.HostSpace;
ps.PageQuota = portal.PageQuota;
ps.UserQuota = portal.UserQuota;
ps.AdministratorRoleId = portal.AdministratorRoleId;
ps.AdministratorRoleName = portal.AdministratorRoleName;
ps.RegisteredRoleId = portal.RegisteredRoleId;
ps.RegisteredRoleName = portal.RegisteredRoleName;
ps.Description = portal.Description;
ps.KeyWords = portal.KeyWords;
ps.BackgroundFile = portal.BackgroundFile;
ps.GUID = portal.GUID;
ps.SiteLogHistory = portal.SiteLogHistory;
ps.AdminTabId = portal.AdminTabId;
ps.SuperTabId = portal.SuperTabId;
ps.SplashTabId = portal.SplashTabId;
ps.HomeTabId = portal.HomeTabId;
ps.LoginTabId = portal.LoginTabId;
ps.UserTabId = portal.UserTabId;
ps.DefaultLanguage = portal.DefaultLanguage;
ps.TimeZoneOffset = portal.TimeZoneOffset;
ps.HomeDirectory = portal.HomeDirectory;
ps.Version = portal.Version;
ps.AdminSkin = SkinController.GetSkin(SkinInfo.RootSkin, portalId, SkinType.Admin);
if (ps.AdminSkin == null)
{
ps.AdminSkin = SkinController.GetSkin(SkinInfo.RootSkin, DotNetNuke.Common.Utilities.Null.NullInteger, SkinType.Admin);
}
ps.PortalSkin = SkinController.GetSkin(SkinInfo.RootSkin, portalId, SkinType.Portal);
if (ps.PortalSkin == null)
{
ps.PortalSkin = SkinController.GetSkin(SkinInfo.RootSkin, DotNetNuke.Common.Utilities.Null.NullInteger, SkinType.Portal);
}
ps.AdminContainer = SkinController.GetSkin(SkinInfo.RootContainer, portalId, SkinType.Admin);
if (ps.AdminContainer == null)
{
ps.AdminContainer = SkinController.GetSkin(SkinInfo.RootContainer, DotNetNuke.Common.Utilities.Null.NullInteger, SkinType.Admin);
}
ps.PortalContainer = SkinController.GetSkin(SkinInfo.RootContainer, portalId, SkinType.Portal);
if (ps.PortalContainer == null)
{
ps.PortalContainer = SkinController.GetSkin(SkinInfo.RootContainer, DotNetNuke.Common.Utilities.Null.NullInteger, SkinType.Portal);
}
ps.Pages = portal.Pages;
ps.Users = portal.Users;
// set custom properties
if (DotNetNuke.Common.Utilities.Null.IsNull(ps.HostSpace))
{
ps.HostSpace = 0;
}
if (DotNetNuke.Common.Utilities.Null.IsNull(ps.DefaultLanguage)) {
ps.DefaultLanguage = DotNetNuke.Services.Localization.Localization.SystemLocale;
}
if (DotNetNuke.Common.Utilities.Null.IsNull(ps.TimeZoneOffset)) {
ps.TimeZoneOffset = DotNetNuke.Services.Localization.Localization.SystemTimeZoneOffset;
}
ps.HomeDirectory = DotNetNuke.Common.Globals.ApplicationPath + "/" + portal.HomeDirectory + "/";
// get application version
string[] arrVersion = DotNetNuke.Common.Assembly.glbAppVersion.Split('.');
int intMajor = 0;
int intMinor = 0;
int intBuild = 0;
Int32.TryParse(arrVersion[0], out intMajor);
Int32.TryParse(arrVersion[1], out intMinor);
Int32.TryParse(arrVersion[2], out intBuild);
ps.Version = intMajor.ToString() + "." + intMinor.ToString() + "." + intBuild.ToString();
}
//Add each portal Tab to DekstopTabs
TabInfo portalTab = default(TabInfo);
ps.DesktopTabs = new ArrayList();
bool first = true;
foreach (KeyValuePair<int, TabInfo> tabPair in tc.GetTabsByPortal(ps.PortalId))
{
// clone the tab object ( to avoid creating an object reference to the data cache )
portalTab = tabPair.Value.Clone();
// set custom properties
if (portalTab.TabOrder == 0)
{
portalTab.TabOrder = 999;
}
if (DotNetNuke.Common.Utilities.Null.IsNull(portalTab.StartDate))
{
portalTab.StartDate = System.DateTime.MinValue;
}
if (DotNetNuke.Common.Utilities.Null.IsNull(portalTab.EndDate))
{
portalTab.EndDate = System.DateTime.MaxValue;
}
ps.DesktopTabs.Add(portalTab);
//assign the first 'normal' tab as the active tab - could be the home tab, if it
//still exists, or it will be after the admin tab(s)
if (first && (portalTab.TabID == portal.HomeTabId || portalTab.TabID > portal.AdminTabId))
{
ps.ActiveTab = portalTab;
first = false;
}
}
//last gasp chance in case active tab was not set
if (ps.ActiveTab == null) ps.ActiveTab = portalTab;
//Add each host Tab to DesktopTabs
TabInfo hostTab = default(TabInfo);
foreach (KeyValuePair<int, TabInfo> tabPair in tc.GetTabsByPortal(DotNetNuke.Common.Utilities.Null.NullInteger))
{
// clone the tab object ( to avoid creating an object reference to the data cache )
hostTab = tabPair.Value.Clone();
hostTab.PortalID = ps.PortalId;
hostTab.StartDate = System.DateTime.MinValue;
hostTab.EndDate = System.DateTime.MaxValue;
ps.DesktopTabs.Add(hostTab);
}
//now add the portal settings to the httpContext
if (System.Web.HttpContext.Current == null)
{
//if there is no HttpContext, then mock one up by creating a fake WorkerRequest
string appVirtualDir = DotNetNuke.Common.Globals.ApplicationPath;
string appPhysicalDir = AppDomain.CurrentDomain.BaseDirectory;
string page = ps.PortalAlias.HTTPAlias;
string query = string.Empty;
System.IO.TextWriter output = null;
//create a dummy simple worker request
System.Web.Hosting.SimpleWorkerRequest workerRequest = new System.Web.Hosting.SimpleWorkerRequest(page, query, output);
System.Web.HttpContext.Current = new System.Web.HttpContext(workerRequest);
}
//stash the portalSettings in the Context Items, where the rest of the DNN Code expects it to be
System.Web.HttpContext.Current.Items.Add("PortalSettings", ps);
return ps;
}