Thus far, we have covered everything but the user interface and the changes you will need to make in your module to actually expose taxonomy/folksonomy in it. Because of this, we simply have to focus on adding existing controls available in the core to your module interface (in the .ascx files) and hooking the controls into your existing load/update methods. As with the other parts of this series, I ask that you please read them prior to reading this blog entry.
You can find the previous parts of the series here:
- Part 1
- Part 2
- Part 3
- Part 4
- Part 5
Edit Entry
Probably named somewhat differently in your module, EditEntry.ascx represents where a single Content Item in your module is created or updated from. This will typically only be seen (or even available to) site administrators and content editors. Remember, this module (and the blog series) focuses on a single module instance with multiple Content Items. Keeping this in mind, I have kept the code samples as simple as possible so you should be able to plug it into your own module with relatively few changes (but it will likely require multiple additions, which your existing module should already have). That aside, lets dive into some code.
Sample EditEntry.ascx:
1:
2:
3:
4:
5: <div class="MyModule-EditEntry">
6: <fieldset>
7: <legend><asp:Label ID="lblLegend" resourcekey="lblLegend" runat="server" />legend>
8: <dl>
9:
10: <dt>
11: <dnn:label id="dnnlblTerms" runat="server" controlname="tsTerms" suffix=":" CssClass="SubHead" />
12: dt>
13: <dd>
14: <dnn:TermsSelector ID="tsTerms" runat="server" Height="250" Width="600"/>
15: dd>
16: <dt><br />dt>
17: <dd>
18: <asp:LinkButton ID="cmdSave" runat="server" CssClass="CommandButton primary-action" resourcekey="cmdSave" OnClick="CmdSaveClick" CausesValidation="true" />
19: <asp:LinkButton ID="cmdDelete" runat="server" CssClass="CommandButton secondary-action" resourcekey="cmdDelete" OnClick="CmdDeleteClick" CausesValidation="false" />
20: <asp:LinkButton ID="cmdCancel" runat="server" CssClass="CommandButton secondary-action" resourcekey="cmdCancel" OnClick="CmdCancelClick" CausesValidation="false" />
21: dd>
22: dl>
23: fieldset>
24: <dnn:Audit id="ctlAudit" runat="server" />
25: div>
The sample above contains two lines we have to focus on here for taxonomy integration: 2, 9. In line 2 we are simply registering the DotNetNuke.Web.UI.WebControls namespace (as you can see, this is found in the DotNetNuke.Web.dll). In line 9, we are using the actual control (Terms Selector). This terms selector control is the same one you see when you edit any page in DotNetNuke and locate the Tags section (in 5.4.x installs and newer). It is basically a drop down with a tree view contained within (both are the DotNetNuke implementation of their Telerik equivalent). With the control now in place in our .ascx file, we can now shift our focus to the corresponding code used to populate the control, which is shown below.
Sample Load Method (MVP Style):
1: public void Refresh()
2: {
3: tsTerms.PortalId = ModuleContext.PortalId;
4: tsTerms.Terms = Model.BlogEntry.Terms;
5: tsTerms.DataBind();
6: }
In line 3 we tell the control which portal we are using (in a non-MVP style module that inherits from PortalModuleBase this would simply be PortalId). This will, in turn, bind a list of available tags and categories to the portal. In line 4, we are setting the terms that have already been assigned to our entity (EntryInfo in this case, retrieved by calling a GetEntry method in our DAL. This then will select the tags/categories in the tree view for you). If you are creating a new entry, this would be an empty list of terms and nothing would be selected in the tree view because of that. Finally, in line 5 we bind the data to the control.
Sample Save Method (MVP Style):
1: private readonly IMyModuleController _controller;
2:
3: protected void CmdSaveClick(object sender, EventArgs e)
4: {
5: Model.MyEntry= new EntryInfo();
6: // Normal assignment code here (omitted)
7:
8: // Save Terms
9: Model.MyEntry.Terms.Clear();
10: Model.MyEntry.Terms.AddRange(tsTerms.Terms);
11:
12: Save(this, new UIEventArgs(Model.MyEntry));
13: }
14:
15: public void Save(object sender, UIEventArgs e) {
16: try {
17:
18: e.Entry.EntryId = EntryID;
19: e.Entry.ModuleId = ModuleId;
20:
21: if (EntryID != Null.NullInteger)
22: {
23: e.Entry.LastModifiedByUserId = UserId;
24: e.Entry.LastModifiedOnDate = DateTime.Now;
25: _controller.UpdateEntry(e.Entry, TabId);
26: }
27: else
28: {
29: e.Entry.CreatedByUserId = UserId;
30: e.Entry.CreatedOnDate = DateTime.Now;
31: e.Entry.LastModifiedByUserId = UserId;
32: e.Entry.LastModifiedOnDate = DateTime.Now;
33:
34: var objEntry = _controller.AddEntry(e.Entry, TabId);
35: _controller.UpdateEntry(objEntry, TabId);
36: }
37:
38: Response.Redirect(Globals.NavigateURL());
39: } catch (Exception exception) {
40: ProcessModuleLoadException(exception);
41: }
42: }
Again, with this sample as well as with the previous sample, non-MVP modules would look very similar to this. In line 8 is where we take what is selected in the user interface, via the tsTerms control, and add it (by using the AddRange, which is available via System.Collections.Generic namespace) to our EntryInfo’s Terms property. In lines 13-40, which would be located in our EditEntryPresenter class, we actually call either the UpdateEntry and AddEntry methods located in our DAL (covered in Part 4) which is what takes care of saving additions/changes to the data store. As you can probably tell we handle both creating and updating scenarios using a single save method.
NOTE:
If you are integrating into your own module, you will find a problem with the core implementation of the Terms Selector control. The problem here is that unless you have a reference to Telerik.Web.UI.dll in your module project (which shouldn’t be done unless you have your own Telerik development license or DotNetNuke Professional which permits you to develop against it). I have added the item to Gemini and it should be fixed for the 5.5 release.
Detail View
The detail view is a single Content Item view which will be seen by the majority of the module users (such as site visitors). We could expose the same items which we will cover here (from taxonomy) that expose multiple content items (ie. a list view) by simply repeating the data binding (shown below) for each Content Item you are showing.
Sample Entry.ascx (MVP Style):
1:
2:
3: <div class="MyModule-Entry">
4: <asp:HyperLink ID="hlEdit" Visible="" runat="server" >
5: <asp:Image ID="imgEdit" runat="server" ImageUrl="~/images/edit.gif" Visible="" resourcekey="Edit" />
6: asp:HyperLink>
7:
8: <div class="EntryTags">
9: <dnn:tags id="tagsControl" runat="server" />
10: div>
11: div>
Similar to our previous ascx file, we register the namespace for the control but now we are using a different control (Tags), which is shown in line 9. For your information, this is the same control used for the skin object and can be seen in the default skin once it was integrated into the Core.
Sample Load Entry.ascx Method (MVP Style):
1: public void Refresh()
2: {
3: tagsControl.ContentItem = Util.GetContentController().GetContentItem(Model.Entry.ContentItemId);
4:
5: int searchTabId;
6: var objModules = new DotNetNuke.Entities.Modules.ModuleController();
7: var searchModule = objModules.GetModuleByDefinition(ModuleContext.PortalSettings.PortalId, "Search Results");
8:
9: if (searchModule != null) {
10: searchTabId = searchModule.TabID;
11: } else {
12: return;
13: }
14:
15: tagsControl.AllowTagging = true & Request.IsAuthenticated;
16: tagsControl.NavigateUrlFormatString = DotNetNuke.Common.Globals.NavigateURL(searchTabId, "", "Tag={0}");
17: tagsControl.RepeatDirection = "Horizontal";
18: tagsControl.Separator = ",";
19: tagsControl.ShowCategories = true;
20: tagsControl.ShowTags = true;
21: tagsControl.AddImageUrl = "~/images/add.gif";
22: tagsControl.CancelImageUrl = "~/images/lt.gif";
23: tagsControl.SaveImageUrl = "~/images/save.gif";
24: tagsControl.CssClass = "SkinObject";
25: }
Because I am using the MVP style it may seem somewhat different at first glance but basically, this would be called on your page load. Now, if we look at line 3 this is where we tell the tag control what Content Item to be associated with (which is retrieved via our GetEntry method of our DAL). This is done by calling the GetContentItem function from the Core API (under the DotNetNuke.Entities.Content.Common namespace) and passing it the ContentItemID we want to retrieve (thus it is the one associated with our entry). Lines 5-13 determine where the Core search module is located in the portal so we can assign it to our tag control in line 16. Backing up one line, line 15, tells the control that we only want to allow tagging for authenticated users. The remainder of the lines, 17-24 simply finish setting up the user interface for the control (I believe all of these have to be set at this time, I imagine this will change in the future to have default property values in the control itself).
Closing
Although it may have appeared to be a lot of things to take in, integrating taxonomy into your module is pretty simple if you read this series first (or dig through the existing samples such as the skin object and edit page as I did). As you start doing it in your module you may find a few hiccups, as I did, but all were easy enough to overcome. If you run into a usability issue, you may want to search Gemini for the terms “taxonomy” or “folksonomy” to see if it exists already (and add it if you can’t find it). I hope, at the very least, this series offered some insight into how to go about integrating taxonomy/folksonomy in your module. I do want to thank Charles Nurse who read over some of the series to ensure things were accurate.
Because this blog series will be here for a long time, I ask that you utilize the building extensions (modules) forum for any questions. I will keep an eye on that forum for any questions related to taxonomy/folksonomy and try to answer them as they are added (for the same reason, I am not using a pinned thread). Please do keep in mind that localization for taxonomy/folksonomy will not really be available until DNN 5.5 is release so please hold off on those particular questions for now.