Showing posts with label Extending. Show all posts
Showing posts with label Extending. Show all posts

9/30/2013

Displaying User Profile photos in other places (with a little help)

Today I ran into a problem I didn't imagine still existed (in DNN v.7.2). I was using Ventrian Property Agent and I needed to display the profile photo of the user who made the submission inside the module's View Item template. I was surprised that neither Property Agent supported that, nor was there another way to get the user's profile image at a decent size inside the module's templates.

 

I'm sure that even if you aren't using this specific module, you've also ran into this issue one way or another. There is no decent built-in way to display a user's profile image outside of the user profile page itself.

 

In my case, there is a token Ventrian Property Agent users named [USERID] that provides the id of the user who has created the current entry. With only this in hand, I had to find a way to use it to get a decent profile picture.

 

First option would be to use the standard DNN /profilepic.ashx image handler, but this one returns distorted, grainy images of max 64x64 pixels. Using it like this: <img src="/profilepic.ashx?userid=[USERID]"> would give me a crappy image, but at least an image.

 

Two minutes before I was ready to give up, I ran into this gem: http://bbimagehandler.codeplex.com/ - this is a generic image handler that does a lot of things - resizing, watermarks, etc. but one interesting feature is that it can render DNN user profile images - in any dimension given, and with awesome quality compared to DNN's own profilepic.ashx.

 

Give it a try if you're facing the same problem. Have in mind that in order for DNN Profile Pictures to work, you'll HAVE to specify the "db" parameter as mentioned in the documentation. By specifying this parameter, it is implied that you have already updated your web.config file with this key:

 

<add key ="BBDatabase" value="Connectionstring=SiteSqlServer;table=MyImages;ImageField=ImageData;idField=ImageID" />

 

The trick is that you only need the first part if you only need BBImageHandler to handle profile images, since it already knows what tables and fields to use. So you can use this version:

 

<add key ="BBDatabase" value ="Connectionstring=SiteSqlServer;" />

 

Where "SiteSqlServer" is your connection string's name (in case you have renamed it from the default name DNN gives it).

 

This way, it is only enough to know the user id whose profile image you need to show - In my case, as I said above, I had the [USERID] token, so my code was as follows:

 

<img src="/bbimagehandler.ashx?db=BBDatabase&userid=[USERID]&portalid=0&width=300">

 

And voila! A profile image with a 300px width inside a Property Agent Template! (I'm thinking of even using it inside the user profile page itself).

 

Until next time!

Read more...

10/21/2011

Adding paging to the core Announcements module

The Announcements module (at the time of writing this, the latest version was 4.0.3) is a very simple DNN core module that lets you present a list of titles and descriptions leading to files, other pages, external links or nowhere at all. It’s customizable via templates, but it doesn’t include any form of paging. So if you have like hundreds of announcements to publish, you probably have to use another module.

 

Not any more, since with a little help from a clever JQuery plugin called Pajinate (download from here), you can add paging to your announcements module. Of course, you’ll be loading all items since Pajinate functions on the client side, but it’s better than not having paging at all.

 

I used the default template that comes with the Announcements module and added what was needed to implement paging on it. Unfortunately, Pajinate does not hide the pager when there’s only one page nor does it hide the First/Previous and Next/Last buttons when we are at the first and last page respectively, so I had to write my own little chunk of Javascript to handle this correctly.

 

Additionally, I adapted the default header and footer templates to include IDs and other stuff needed for pajinate to work correctly. Those IDs are used in the additional javascript code I wrote.

 

So here’s the Javascript code. I suggest you put this code on a separate file (I used pajinatehelper.js) and load it AFTER the pajinate javascript file. You will probably need to change the value of the itemsperpage variable (top of script) to your own preference, as well as the literals for the First/Previous/Next/Last buttons that follow.

//Set the number of items per page

 

var itemsperpage = 10;
$(document).ready(function(){
$('#paging_container').pajinate( {
item_container_id : '#pagetable', //This must be left as is
items_per_page: itemsperpage,
nav_label_first: 'First',//Change this to whatever you like
nav_label_prev: 'Prev', //Change this to whatever you like
nav_label_next: 'Next', //Change this to whatever you like
nav_label_last: 'Last' //Change this to whatever you like
});

//Get the total number of items
var total_items = $('#pagetable').children().size()

//Get the number of pages
var numberofpages = Math.ceil(total_items/itemsperpage);

//Since we start at page 1, hide First/Previous buttons
hideFirstPrev();


//Decide what to hide when user clicks on a page number.
$('#paging_container').find('.page_link').click(function(){

var currpage = parseInt($(this).attr('longdesc'));
currpage+=1; //longdesc is 0-based but page numbers start at 1.

if (currpage==1) {
hideFirstPrev();
}

else if (currpage==numberofpages) {
hideNextLast();
}

else

{
showAll();
}

});

//Hide First and Previous links when the user clicks the First link
$('#paging_container').find('.first_link').click(function(){
hideFirstPrev();
});

//Hide Last and Next links when the user clicks on the Last link
$('#paging_container').find('.last_link').click(function(){
hideNextLast();
});

//Decide what to hide when the user clicks on the Next or Previous links
$('#paging_container').find('.previous_link,.next_link').click(function(){

var currpage = parseInt($('#paging_container').find('.active_page').attr('longdesc'));
currpage+=1; //longdesc is 0-based but page numbers start at 1.

if (currpage==1) {
hideFirstPrev();
}
else if (currpage==numberofpages) {
hideNextLast();
}
else {
showAll();
}

});


//If we have only one page, disable pager completely.
if(total_items<itemsperpage){
$('.page_navigation').hide();
}

});

//These are helper functions for hiding / showing First/Last/Next/Previous links

function hideFirstPrev() {
toggleFirstLastNextPrevControls(1);
}

function hideNextLast() {
toggleFirstLastNextPrevControls(2);
}

function showAll() {
toggleFirstLastNextPrevControls(3);
}

function toggleFirstLastNextPrevControls (mode) {

if (mode==1) {
$('#paging_container').find('.first_link').hide();
$('#paging_container').find('.previous_link').hide();
$('#paging_container').find('.last_link').show();
$('#paging_container').find('.next_link').show();
}

else if (mode==2)
{
$('#paging_container').find('.first_link').show();
$('#paging_container').find('.previous_link').show();
$('#paging_container').find('.last_link').hide();
$('#paging_container').find('.next_link').hide();
}

else if (mode==3)

{
$('#paging_container').find('.first_link').show();
$('#paging_container').find('.previous_link').show();
$('#paging_container').find('.last_link').show();
$('#paging_container').find('.next_link').show();
}
}






Here’s the code for the Header Template for the Announcements module:




<link rel="stylesheet" type="text/css" href="/portals/0/scripts/pajinate.css"/>
<script type="text/javascript" src="/portals/0/scripts/jquery.pajinate.js">
</script>
<script type="text/javascript" src="/portals/0/scripts/pajinatehelper.js"></script>

<div id="paging_container" style="text-align:left;">
<table class="DNN_ANN_DesignTable" cellspacing="0" summary="Announcements Design Table" border="0" style="border-collapse:collapse;"><tr><td>
<div id="pagetable">





Please note that I include the stylesheet (pajinate.css), the js file for Pajinate (jquery.pajinate.js – you can use jquery.pajinate.min.js too) and my own .js file (pajinatehelper.js) from my /portals/0/scripts folder (I actually created that “scripts” folder, since it didn’t exist in the default skin’s folder structure). You, of course, can load them from whatever path you like, or you can even inject them in the page’s HEAD section (see this post for more on that).



Pajinate demands that the items to be paged are enclosed inside an element with a specific id (in our case, “paging_container”). You can see the complete documentation here. Unfortunately, this doesn’t work well with tables, so I used an enclosing DIV with this id. Of cource, in your implementation, you can ditch the TABLE alltogether and just use DIVs. I tried to alter the default template as little as possible.



Pajinate also needs a second element that contains elements to be paged. I used this also, as a div with “pagetable” id. Those two are configurable (see the script)



And here’s the code for the footer:




</div>
</td></tr></table>
<div class="page_navigation"></div>
</div>





The “page_navigation” div is once again required by pajinate in order to show the pager.



Phew. When you put all the code in place, and ensure that it’s working correctly, the result will be something like this:



pajinate



The orange pager comes from the default stylesheet that comes with Pajinate. You can, of course, change it at your will. In this example, I used a page size of 2.



CAUTION: Since errors in Javascript can sometimes render your page useless and even disable the edit controls, please take a bookmark of the module edit page URL before you apply changes, so you can always get back to it in case things go wrong.



Have fun and good luck!

Read more...

11/17/2010

Using resource files in embedded user controls in skins

If you are editing ASCX skins, then you have probably been tempted to introduce a bit of reusability by creating custom user controls and embedding them to your skins. These can actually function like skin objects, but without all the fuss. I’ve found it easy to create a couple of those in cases where I may need, for example, a common footer for all my skins, but of course you can do a great deal more with them, like introduce functionality without having to copy all the code to every skin file you make.

 

In any case, you have to register your control inside your skin file like this (names of folders, portal and skin path and tagprefix/name are arbitrary):

 

<%@ Register TagPrefix="MyCompany" TagName="FOOTER" src="~/Portals/0/Skins/myskin/myskinobjectsfolder/footer.ascx" %>

 

And use it in your skin file like this:

 

<MyCompany:FOOTER id="myFooterId" runat="server" />

 

The question is: How do you localize these little bastards? Even if you add DNN controls on them, the ResourceKey property continues to get data only from the skin’s resource file, not the control’s.

 

Of course, you can use your skin’s resource file to get localized strings and other data but this isn’t very convenient since you’ll have to add data to every resource file corresponding to each one of your skins. And I’m not going to talk about app_globalresources either! :)

 

(If you don’t know that you can have resource files for your skins, yes, you can. You can have a skins/app_localresources folder and you can have .resx files there  and you can use the ResourceKey attribute in your DNN control tags to get localized data for your controls in your skins).

Well, based on this excellent post here about localizing embedded controls, I thought it would be good to summarize and simplify the process a bit. So here it goes:

 

You can give an embedded user control its own resource file by following two different approaches:

 

1. Make it inherit PortalModuleBase

2. Specify the resource file yourself and make the control use it.

 

Approach 1: Inheriting PortalModuleBase

You just have to add:

 

Inherits="DotNetNuke.Entities.Modules.PortalModuleBase"

 

in your control’s header.

E.g.

 

<%@ Control language="vb" AutoEventWireup="false" Explicit="True" Inherits="DotNetNuke.Entities.Modules.PortalModuleBase" %>

 

Doing so will give you access to the LocalResourceFile property, which will in turn point to the .resx file located in the app_localresources folder right under your control’s location.

 

But what .resx file, exactly? Not the one you expected.

 

The resx file will NOT follow the file name of your user control, but the ID by which it is called. If your control’s filename is MyFooter.ascx, and you call it with an ID of “myFooterId” then your resource file must be named “myFooterId.ascx.resx”.

Then, you can use code like this to get a localized value, for example, for the “Footer.Text” key:

 

<%=DotNetNuke.Services.Localization.Localization.GetString("Footer.Text", LocalResourceFile)

%>

Ugly, because you have to remember to always use the same ID and also because you can’t use two instances of your control in the same skin. Also, inheriting PortalModuleBase restricts your coding flexibility.

 

Approach 2: Forcing the user control to use a resource file

 

If you don’t want your user control to inherit PortalModuleBase but you still need it to have its own resource files, you can define a LocalResourceFile property yourself. This is written directly in the .ASCX file but, of course, you can put it in code-behind too. (sorry about the line breaks):

 

<script runat="server">
Public ReadOnly Property LocalResourceFile As String
     Get
         Return Me.TemplateSourceDirectory & _
     "/" & _
DotNetNuke.Services.Localization.Localization.LocalResourceDirectory & _
     "/" & _
     System.IO.Path.GetFileNameWithoutExtension(me.AppRelativeVirtualPath)
     End Get
End Property

Public Function GetLocalizedString ( _

ByVal key as string) as string
    Dim retVal as String
    retVal = _
DotNetNuke.Services.Localization.Localization.GetString( _
    key, LocalResourceFile)
    return (retVal)
End Function

</script>

 

What we did here was to create our own LocalResourceFile property. This tells the control to use a resource file that follows the file name of the control itself. So, if your control is named footer.ascx, your resource file will lie in app_localresources\footer.ascx.resx. If you need to specify a differently – named resource file, you just replace the GetFileNameWithoutExtension call with your own string. E.g. if you use “myownFooter” there, then there must be a myownFooter.ascx.resx file in the app_localresources folder.

 

In addition, we created a GetLocalizedString function which will just take the key and return the string from the resource file.

We can use this function in our user control to get localized data as follows:

 

<%=GetLocalizedString("Footer.Text")%>

 

This is much simpler and much more flexible.

Read more...

7/04/2010

(Not a) tutorial: Creating my first DNN scheduled task

In this post, I’ll describe my (successful) attempt to create a basic email queue functionality using a custom DNN scheduled task.

 

Why not a tutorial? Because my intention is to show you what I have done, without telling you that this is the right (or the only) way to do it, or demonstrating this as a techically complete and correct solution. This work is far from finished – it just covers basic functionality and is not usable out of the box. But if you’re struggling with DNN scheduled tasks, reading below may provide you with a way to get started.

 

I found great help in this excellent post here, which you should read for more information on creating your own scheduled tasks in DNN. Actually, this article here is a simple application of the tutorial I found in that post. Read it, it’ll be extremely helpful.

 

Let’s get to the point.

 

Purpose:

 

I had the need to notify custom groups of DNN registered users upon submission of some forms created either with the XMOD Pro or the Datasprings Dynamic Forms module. Although both modules support sending emails upon submission, they are restricted to sending emails to specific people, lists, or roles.

 

I needed something more complex, such as a database query based on several tables, in order to determine who would get the notification. Both modules allow issuing queries to the database upon submission, so it seemed right to create my own email queue, consisting of a database table (the queue) and a scheduled task to send the mails.


What’s more, a queue can help where there is a bunch of emails to be sent – let’s say you need to send 1k emails upon the next form submission – you don’t have to send them all at once. You just send a dozen each time the scheduled task is ran, until the queue is empty.

 

Database objects

 

So – what did I need first? Of course, a table to hold my queue.

 

CREATE TABLE [dbo].[DotSee_MailQueue]( 
[MailID] [int] IDENTITY(1,1) NOT NULL,
[FromUser] [nvarchar](500) NOT NULL,
[ToUser] [nvarchar](500) NOT NULL,
[Subject] [nvarchar](500) NOT NULL,
[BodyHtml] [nvarchar](max) NULL,
CONSTRAINT [PK_DotSee_MailQueue] PRIMARY KEY CLUSTERED
(
[MailID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY]

GO


Yes, it’s a silly table but enough for my basic functionality: From, To, Subject, Body and an id. All the necessary information to send an email.


Then I needed a stored procedure to retrieve information from that table:



CREATE PROCEDURE [dbo].[DotSee_GetMailQueueItems] 
AS
BEGIN
SET NOCOUNT ON;

Select top 10 [MailID],[FromUser],[ToUser],[Subject],[BodyHtml] FROM [dbo].[DotSee_MailQueue]

END

GO


Note: I prefix my tables with “DotSee_” since this is the name I have selected for my custom work (namespaces included, as you’ll see below). Don’t give it serious attention, it’s just my convention.


Setting up a project for my scheduled task


There are more than one ways to begin creating a scheduled job for DotNetNuke using Visual Studio, but there is a common denominator: Your job MUST be a DLL file in DotNetNuke’s bin directory. So you can go either with a Web Application Project or with a Class Library. I chose Class Library. Here’s what I did:



  1. Created a new Class Library project.

  2. Added a reference to DotNetNuke.dll from an existing DNN installation (and set Copy Local to false)

  3. Added a reference to Microsoft.ApplicationBlocks.Data from the same DNN installation (in order to be able to use DNN’s own DAL for interacting with the database)

  4. Set the build path INSIDE DNN’s bin folder to save me time copying and recopying the dll.

  5. Went to the project’s properties and deleted the contents of the root namespace field. Yes, it should be empty for your dll to work. Don’t miss that.



sche1


Regarding (4), I did this work on a virtual machine, where I had a DNN installation and my class library project on the same computer. I suggest you follow some similar approach, since it is very easy to test and debug (you can debug only by attaching to the w3wp.exe process, but it’s better than nothing).


Writing the code


Two more things you must know: Your custom class must inherit DotNetNuke.Services.Scheduling.SchedulerClient and the only method that is called by DNN’s scheduler is DoWork().


So here’s my custom scheduler class:



Imports DotNetNuke.Services.Scheduling 
Imports DotNetNuke.Services.Exceptions

Imports System
Imports System.Web
Imports System.IO

Namespace DotSee.DnnScheduledMailNotifier

Public Class Mailer
Inherits SchedulerClient

' requires a special constructor which
' accepts a ScheduleHistoryItem
Public Sub New( _
ByVal objScheduleHistoryItem _
As ScheduleHistoryItem)

MyBase.New()
Me.ScheduleHistoryItem = objScheduleHistoryItem

End Sub

Public Overrides Sub DoWork()
Try
' perform some tasks
MailWorker.DoWork()

' report success to the scheduler framework
ScheduleHistoryItem.Succeeded = True

Catch ex As Exception

ScheduleHistoryItem.Succeeded = False
ScheduleHistoryItem.AddLogNote _
("EXCEPTION" + ex.ToString())
Errored(ex)
Exceptions.LogException(ex)

End Try
End Sub
End Class

End Namespace


Apart from the MailWorker.DoWork() line (code coming right below), the class can be used as a template for writing your own scheduled tasks. (You will use your own namespace, of course). I put my specific code in a different class so that it would be easy to make the distinction between code needed for my specific need and the “template” code that every DNN scheduled task needs.


Here’s my MailWorker class (DotNetNuke.dll referenced was version 5.2.1, GetHostSettingsDictionary may not work with 4.x):



Imports DotNetNuke.Services.Mail
Imports DotNetNuke
Imports DotNetNuke.Data
Imports DotNetNuke.Entities.Host


Namespace DotSee.DnnScheduledMailNotifier


Public Class MailWorker

Private Sub New()

End Sub

Public Shared Sub DoWork()

Dim d As Dictionary(Of String, String)
d = Host.GetHostSettingsDictionary()

Dim dnp As DataProvider
dnp = DataProvider.Instance
Dim dr As IDataReader

Dim ids As New List(Of Int32)

dr = dnp.ExecuteReader( _
"DotSee_GetMailQueueItems", Nothing)

If Not dr.Read() Then Return

While dr.Read

Mail.SendMail( _
dr("fromuser").ToString _
, dr("touser").ToString _
, "" _
, dr("subject").ToString _
, dr("bodyhtml").ToString _
, "" _
, "" _
, d("SMTPServer") _
, d("SMTPAuthentication") _
, d("SMTPUsername") _
, d("SMTPPassword") _
)

ids.Add(CType(dr("mailid"), Int32))

End While

For Each id In ids
dnp.ExecuteSQL( _
String.Format( _
"DELETE FROM DotSee_MailQueue WHERE mailid={0}" _
, id.ToString))
Next

End Sub

End Class

End Namespace


What it actually does (in a brutal way, I should add) is check the table, get 10 items at a time (via the DotSEe_GetMailQueueItems stored procedure defined earlier), send an email for each of the items and then delete the corresponding records from the table. (Ok, I know the way it does it sucks a bit, but It’s only quick and dirty functionality, remember).


So when I compile this beauty, my DotSee.DNNScheduledMailNotifier.dll goes inside DNN’s bin folder, and now what I have to do is tell DNN that this is a scheduled task and that the scheduler should include it. Here’s how:



  1. Host-Schedule and click “Add Item to Schedule”.

  2. Set friendly name to whatever you want

  3. Full class name and assembly should be your assembly name and your class name (the class containing the DoWork method) combined, as in AssemblyName.Classname. My Assembly’s name is DotSee.DnnScheduledMailNotifier and my class name is Mailer, so the full name is DotSee.DnnScheduledMailNotifier.Mailer.

  4. Check the “schedule enabled” checkbox

  5. Time lapse MUST have some value or else the task will not run at all.



sche2



That’s it! The task is ready to run. I tested it by manually inserting records into the DotSee_MailQueue table and it worked well, even with hundreds of records, sending 10 emails every 5 seconds.


So now I have my basic system ready. When I need to send emails to groups of people not defined by roles or other DNN constructs, I just have to write a good SQL query that fills the table with recipients that correspond to the criteria I need every time and presto!


Of course, there’s plenty of room for configuration / parameterization (for example, the number of emails sent each time or the type of email – html or plain text, or even a check for whether delivery was successful). But I guess you got the idea.


Thank you for your time.

Read more...

4/21/2009

UDT 03.05.01 and incorrect filtering of script in text/html fields

Those of you using the latest version of the old User Defined Table module (which is to be replaced by the Form and List module in DNN 5.x) may have noticed an annoying bug that makes the UDT module insist on filtering out script, object and embed tags even when you have specifically specified that you don’t want it to.

 

What does this mean? It means that you can’t embed YouTube videos, for example, because UDT will filter out the embed code’s tags.

 

When does it happen? Well, fortunately it doesn’t happen when you’re admin or host. But it does happen if you’ve given other users edit rights on UDT data. Any user that is not Admin or Host cannot write these tags in the HTML view of a Text/HTML field inside a User Defined Table. They are just filtered out, even when the relevant setting is unchecked.

 

To fix that, you’ll need to download the code for UDT 03.05.01. The problem lies in file EditControls.vb, line 52:

 

If ParentModule.Settings.ContainsKey(SettingName.ForceInputFiltering) Then
               
inputFilterScript = Not isAdmin
               
inputFilterTags = isAnonymous
End If

 

The “if” clause in line 52 just checks whether the setting exists, not its value. To fix that you need to make the code in line 52 as follows:

 

If ParentModule.Settings.ContainsKey(SettingName.ForceInputFiltering) AndAlso ParentModule.Settings(SettingName.ForceInputFiltering).Equals("True") Then
               
inputFilterScript = Not isAdmin
               
inputFilterTags = isAnonymous
End If

 

(I apologise for the line breaks, please consider that the IF clause should be in a single line)

 

What I’ve done is that I have added a second condition that actually checks the VALUE of the setting and applies the rule only if it’s true.

 

You can compile the code with that change and just replace the old dll file with the newly-compiled one in dotnetnuke’s Bin folder.

 

Please let me know if you’ve been having trouble with this, I’ve already got a compiled dll so I could send it to anyone who’s not into code much – although, as always, I must insist that you always try those things at your own risk!

.

Read more...

1/16/2009

Using the PortalSettings class in containers

It’s simple, but useful: If you try to use the PortalSettings class in code you have added to a container’s  .ascx file, you’ll find out that you can’t. The PortalSettings shared class is only directly accessible from skins.

 

That’s dissapointing, especially when you need to grab things like, for example, the current tab id (PortalSettings.ActiveTab.TabId) inside a container for some reeson.

 

The solution to this is to grab your PortalSettings contents from the HttpContext and use it like this:

 

<script runat="server"> 

Dim _portalSettings As PortalSettings 
_portalSettings = CType( _
   
HttpContext.Current.Items("PortalSettings") _
   
, PortalSettings)

</script>

 

In order to get, for example, the current tab id from your container, you can now use _portalSettings.ActiveTab.Tabid.

Read more...

12/05/2008

How to enable - disable caching programmatically and on-demand

Sometimes, when you're doing something programmatically, like moving tabs around, you really don't want caching in your way in any form, since there's a great danger it'll affect the outcome. For some reason, programmatically clearing the cache may not be enough - you just don't need any caching around when you do certain stuff, and you don't want to oblige any host user to manually clear the cache. Here's a really simple way to disable caching programmatically.

 

 

'Get the initial caching level
'from the HostSettings arraylist 
'(this is the easiest way)
Dim initialCachingLevel As String
initialCachingLevel = _
   
HostSettings.Item("PerformanceSetting").ToString

'Disable caching
Dim hc As New Entities.Host.HostSettingsController
hc.UpdateHostSetting("PerformanceSetting", "0")

'Do your stuff here

'Enable caching with
'the initial caching level
hc.UpdateHostSetting("PerformanceSetting", initialCachingLevel)

 

What I'm doing in the bit of code above is disable caching temporarily to do what I have to do, then return it to its previous state. That's equivalent to going to Host Settings-Performance, from the Host menu, change the caching level, click "update", do any job I have to do and then get back to the Host menu and return caching to its previous value.

 

The caching level is defined by the "PerformanceSetting" Host property which can take string values ranged from "0" to "3" representing different levels of caching:

 

0 - No caching

1 - Light caching

2 - Moderate caching

3 - Heavy caching

 

Some notes:

 

You can get the current caching level from the HostSettingsController too, but it's a bit more fussy since the GetHostSetting method returns an IDataReader. Since the Host's settings get copied to the publicly-available HostSettings hashtable, I thought it's easier to obtain the current setting from there.

 

Changing the value in the HostSettings hashtable will NOT affect your caching. You've got to use HostSettingsController.UpdateHostSetting.

 

You may also want to clear any cache left before you start doing your stuff, it helps in some cases. In this case, the DataCache class may prove very useful. Here's what you can do (you can choose not to execute some of these lines, depending on your needs):

 

DataCache.ClearTabsCache(0)
DataCache.ClearPortalCache(0, True)
DataCache.ClearHostCache(True)
DataCache.ClearModuleCache()
DataCache.ClearTabPermissionsCache(0)
DataCache.ClearUserCache( _
       
0, _
       
UserController.GetCurrentUserInfo().Username)

 

 

Some of the above statements may overlap, in the sense that they're actually subsets of other statements. For example, ClearHostCache() is considered equivalent to the Clear Cache option that exists in the Host Settings page. I personally prefer to issue ALL those statements, even if they overlap.

 

The DataCache class has some more methods but I think those mentioned here are enough to give you a good start. You'll probably find out the rest yourself, in an as-needed basis.

 

The above code applies only to portal 0. Whenever you see a boolean parameter there, it's indicating "cascade" cache clearing, which I always want set to true. Also, the ClearUserCache() method needs the currently logged on user's name, which I get by using UserController.GetCurrentUserInfo() which returns a UserInfo object containing, among others, the user's name.

Read more...

11/23/2008

Persisting large values in Module Settings when creating a custom module

So you want to create a simple custom module. At first, it seems easy. Just two or three settings to take care of, no need for complicated, custom tables and unistall/uninstall scripts. DNN's built-in module settings API seems to be enough. But suddenly, needs grow a lot. There are some large values you need to store and the ModuleSettings table won't allow large chunks of information per row. It seems that you have to switch to a custom table which will be holding your settings...

 
...or use my class :)

 

I came upon the need of storing just one large value (a serialized object, to be more specific) in a custom module I was creating. I didn't have any way to know how large the serialized (XML) sting would be, since the object was a collection, and I HAD to store it in module settings. I thought it wouldn't be worth to create a custom table and the accompanying code to store settings in a case like this, so I decided to extend DNN's ModuleController class instead.


What I did was to write a new ModuleControllerExtended class which inherits from ModuleController (so I could use it in its place) and add two methods to it:


1. UpdateLargeTabModuleSetting: This method is essentially an extension of the UpdateTabModuleSetting method of the ModuleController class which allows storing large string values in the ModuleSettings table by breaking them into smaller chunks and storing multiple name/value pairs. If a value is small enough to fit into a single row of the ModuleSettings table, then it's stored normally, as it would with the UpdateTabModuleSetting method.


When the value is large, it's stored in 2KB chunks in several rows using a numeric prefix (in the form of _x) for each row, based on the initial name given for the setting. For example, a setting named "SerializedObject" with a value sized at 5KB would be stored in 3 rows with names "SerializedObject_0", "SerializedObject_1" and "SerializedObject_2" accordingly.


The method also takes care to delete rows from the ModuleSettings table every time an update takes place to ensure that there are no leftovers should you specify a value of a smaller size (and probably fewer chunks) than the one that may be already stored.

 

2. ReadLargeTabModuleSetting: This one reads a large setting doing all the work needed to give you back its string value, but it can also read single-row settings. This is a shared method, and it was added to the class only for consistency. It does not extend any of the known methods of the ModuleController class, meaning you can always remove it from the definition of the class and use it as stand-alone code.

 

To use the method, you will need the hashtable containing the module's settings, which you can easily get by using the base class GetModuleSettings() method. You feed the method with the hashtable, the module's id and the name of the setting you want and you get a string value containing the "large" value for the setting you specified, or just a setting value should the setting be a "normal" one.

 

It's not really complicated and it should save you a lot of time when dealing with situations like the one I described above. I would love to hear your comments, though.

 

Here's the code:

 


Imports Microsoft.VisualBasic
Imports System.Collections.Generic

Public Class ModuleControllerExtended
   
Inherits DotNetNuke.Entities.Modules.ModuleController

Public Sub New()
   
MyBase.new()
End Sub

Public Sub UpdateLargeTabModuleSetting( _
  
ByVal tabModuleSettings As Hashtable _
 
, ByVal tabModuleID As Int32 _
 
, ByVal settingName As String _
 
, ByVal settingValue As String)

   
Dim cntDel As Int32 = 0
   
Dim o As Object
   
Dim continueDeleting As Boolean
   
continueDeleting = True

   
'Delete all multiple-value module settings, if they exist. 
    While continueDeleting = True
       
o = tabModuleSettings(settingName + "_" + cntDel.ToString)
       
If Not o Is Nothing Then
           
DeleteTabModuleSetting(tabModuleID, settingName + "_" + cntDel.ToString)
           
cntDel += 1
       
Else
           
continueDeleting = False
       
End If
   
End While

   
'Guard - if setting value is less than 2KB, update normally and exit 
    If settingValue.Length < 2000 Then
       
'Normal value 
        UpdateTabModuleSetting(tabModuleID, settingName, settingValue)
       
Exit Sub
   
End If

   
'If we get to this point, then setting value is more than 2KB. 
    'Delete the original setting (if it exists) so as not to get confused. 
    DeleteTabModuleSetting(tabModuleID, settingName)


   
'Split the value in 2KB chunks 
    Dim stringList As New List(Of String)
   
Dim sb As New StringBuilder(settingValue)

   
While sb.Length >= 2000
       
stringList.Add(sb.ToString.Substring(0, 1999))
       
sb.Remove(0, 1999)
   
End While

   
'Add the last chunk 
    If sb.Length > 0 Then stringList.Add(sb.ToString)

   
'Now do the update changing the setting name with the suffix _x (x=0,1,2,etc.) for 
    'each update 
    Dim cnt As Int32 = 0
   
For Each s As String In stringList
       
UpdateTabModuleSetting(tabModuleID, settingName + "_" + cnt.ToString, s)
       
cnt += 1
   
Next

End Sub

Public Shared Function ReadLargeTabModuleSetting( _
    
ByVal tabModuleSettings As Hashtable _
   
, ByVal tabModuleID As Int32 _
   
, ByVal settingName As String) As String

   
'Guard - if there is a single setting, just return that and exit 
    Dim objTester As Object
   
objTester = tabModuleSettings(settingName)
   
If Not objTester Is Nothing Then
       
Return (CType(objTester, String))
   
End If

   
'If we got to this point, there's a large value stored. 
    'Loop through the records and reconstruct the value. 
    Dim sb As New StringBuilder
   
Dim cnt As Int32 = 0
   
Dim o As Object
   
Dim continueAdding As Boolean
   
continueAdding = True

   
While continueAdding = True
       
o = tabModuleSettings(settingName + "_" + cnt.ToString)
       
If Not o Is Nothing Then
           
sb.Append(CType(o, String))
           
cnt += 1
       
Else
           
continueAdding = False
       
End If
   
End While

   
Return sb.ToString

End Function

End Class

Read more...
Related Posts with Thumbnails

Recent Comments

Free DotNetNuke Stuff

Free DotNet Videos

  © Blogger template The Professional Template by Ourblogtemplates.com 2008

Back to TOP