Rapid Module Development Part 2 - The multilanguage thing…

Since I get to know DNN back in 2008, one subject grinds my gears all the time and this is the multilanguage feature of modules. Back In 2008/2009 there were a lot of rumors and discussions about ML. The corp started with building their multilanguage solution and I remember some intense discussion with Sebastian and others about this topic. At the end the corp solution does not help me building my ML modules and so I had to find my own way to handle this.

In this blog I’ll show you how you can implement ML features without any hassle. My method is very straight forward and you could use this as a pattern for all your modules. There is no more increased effort needed to make your module ML compliant if you follow these guidelines.

The sample module

We want to build a product module in this tutorial. To keep it simple, we bind the product to the module: Every module shows only his module-specific product. In the action menu, we’ll have the option to edit the product. All text properties of the product should be ML capable.

Database Design

It is good practise to prefix your table names with something. My table names  start with the module name as prefix followed by an underline and the real table name. In this sample we have a product table with some fields that are language independent and a compagnion table with the same name and the Suffix **Lang **that contain all the language dependent fields. As the third part in this game we define an SQL View containing all the fields joined from both tables for display purposes.

Database Design

After the design phase  I let my favourite tool XCase  generate the needed SQL code to create everything in SQL server:

Generated SQL

IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'{databaseOwner}[{objectQualifier}BBLanguagePattern_Product]') and OBJECTPROPERTY(id, N'IsTable') = 1)
      CREATE TABLE {databaseOwner}[{objectQualifier}BBLanguagePattern_Product] ( 
         ProductId INT NOT NULL IDENTITY (1,1),
         ModuleId INT NULL,
         Image NVARCHAR(120) NULL,
         Price DECIMAL(12,4) NULL,
         Tax CHAR(10) NULL
      ALTER TABLE {databaseOwner}[{objectQualifier}BBLanguagePattern_Product] ADD CONSTRAINT PK_BBLanguagePattern_Product PRIMARY KEY NONCLUSTERED  (ProductId ASC) WITH ( IGNORE_DUP_KEY = OFF)
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'{databaseOwner}[{objectQualifier}BBLanguagePattern_ProductLang]') and OBJECTPROPERTY(id, N'IsTable') = 1)
      CREATE TABLE {databaseOwner}[{objectQualifier}BBLanguagePattern_ProductLang] ( 
         ProductId INT NULL,
         Language CHAR(5) NULL,
         Name NVARCHAR(40) NULL,
         Shortdescription NVARCHAR(400) NULL,
         Longdescription NVARCHAR(MAX) NULL
IF NOT EXISTS (SELECT 1 FROM sys.objects where name='FK_ProductLang' and type='F')
   ALTER TABLE {databaseOwner}[{objectQualifier}BBLanguagePattern_ProductLang] WITH NOCHECK ADD CONSTRAINT FK_ProductLang FOREIGN KEY ( ProductId ) REFERENCES {databaseOwner}[{objectQualifier}BBLanguagePattern_Product] ( ProductId ) ON DELETE CASCADE
CREATE VIEW BBLanguagePattern_ProductLoc AS
SELECT  ALL    BBLanguagePattern_Product.ProductId , BBLanguagePattern_Product.ModuleId ,  
      BBLanguagePattern_Product.Image , BBLanguagePattern_Product.Tax ,  
      BBLanguagePattern_Product.Price , ProductLang.Language , ProductLang.Name ,  
      ProductLang.Shortdescription , ProductLang.Longdescription 
   FROM BBLanguagePattern_Product INNER JOIN BBLanguagePattern_ProductLang ProductLang ON 
     BBLanguagePattern_Product.ProductId = ProductLang.ProductId

I paste this code into the host/SQL window of DNN and let it create the tables and the view.

Setting up the project

Within Visual studio we create a new project depending on my Bitboxx DNN 7 project template. Project name is equal to the prefix of my database tables – BBLanguagePattern in this case. If the prefixes differ from the project name, you have to edit the file Models/Generated/Database.tt and alter the value in prefix.

// Settings
ConnectionStringName = "SiteSqlServer";            // Uses last connection string in config if not specified
Namespace = "Bitboxx.DNNModules.BBLanguagePattern";
RepoName = ".";
GenerateOperations = false;
GeneratePocos = true;
GenerateCommon = false;
ClassPrefix = "";
ClassSuffix = "";
TrackModifiedColumns = false;
IncludeViews = true;
// Read schema
Tables tables = LoadTables();
string prefix = "BBLanguagePattern";

If everything is OK, saving the Database.tt file now generates Database.cs containing all the needed POCO’s for our tables and for the view:

using System;
using System.Web.Caching;
using Bitboxx.DNNModules.Controls;
using DotNetNuke.ComponentModel.DataAnnotations;
namespace Bitboxx.DNNModules.BBLanguagePattern
	[Cacheable("BBLanguagePattern_Product", CacheItemPriority.Normal, 20)]
    public partial class ProductInfo     
        public int ProductId { get; set; }
        public string Image { get; set; }
        public decimal? Price { get; set; }
        public string Tax { get; set; }
        public int? ModuleId { get; set; }
	[Cacheable("BBLanguagePattern_ProductLang", CacheItemPriority.Normal, 20)]
    public partial class ProductLangInfo : ILanguageEditorInfo     
        public int? ProductId { get; set; }
        public string Language { get; set; }
		[LanguageEditor("TextBox" , MaxLength = 40)]
        public string Name { get; set; }
		[LanguageEditorAttribute("TextBox" , MaxLength = 400)]
        public string Shortdescription { get; set; }
		[LanguageEditorAttribute("TextEditor", Height = "600px")]
        public string Longdescription { get; set; }
	[Cacheable("BBLanguagePattern_ProductLoc", CacheItemPriority.Normal, 20)]
    public partial class ProductLocInfo     
        public int ProductId { get; set; }
        public int? ModuleId { get; set; }
        public string Image { get; set; }
        public string Tax { get; set; }
        public decimal? Price { get; set; }
        public string Language { get; set; }
        public string Name { get; set; }
        public string Shortdescription { get; set; }
        public string Longdescription { get; set; }

Please take a look at the POCO of the ~Lang table. This one is inherited from the ILanguageEditorInfo, an interface specifically for the LanguageEditor to work later. I tweaked the PetaPoco tt files to generate this automatically if we have a table and its Lang counterpart. Also the string properties in the ~Lang POCO now have attributes defining which type of editor control is used to edit (TextBox,TextEditor) and some other properties (MaxLength,Width, Height,Rows,Label).

The LanguageEditor Control

Visual Studio does not know the attributes and the interface at the moment, so now we have to add the Language Editor Control to the project. The easiest way to do this is to download it from here and extract all files into a new project folder named Controls. Then open this folder in Windows Explorer, mark all files insides and do a drag&drop action into the project:

Language Editor Control

The View.ascx

In this part of the Rapid Module Development Blog we will only have a fixed display of our product. The templating will be part of another blog post introducing the template control.

Thats how it should look alike:


The ascx-code is simple:

<div id="bblanguagepattern-view">
    <asp:Image runat="server" ID="imgProduct"/>
    <h3><asp:Label runat="server" ID="lblName"/></h3>
    <p><strong><asp:Label runat="server" ID="lblShortDescription"/></strong></p>
    <p><asp:Label runat="server" ID="lblLongDescription"/></p>
    <div style="float:right;text-align:right;background-color:;padding:40px 20px;;">
        <span class="price"><asp:Label runat="server" ID="lblPrice"/></span><br/> 
        <asp:Label runat="server" ID="lblTax"/>

Lets have a look at the code behind:

public partial class View : PortalModuleBase, IActionable
    #region Event Handlers
    /// <summary>
    /// Runs when the control is loaded
    /// </summary>
    private void Page_Load(object sender, EventArgs e)
            if (!Page.IsPostBack)
                BBLanguagePatternController controller = new BBLanguagePatternController();
                ProductLocInfo product = controller.GetProductLoc(ModuleId, Thread.CurrentThread.CurrentCulture.Name);
                lblName.Text = product.Name;
                lblShortDescription.Text = product.Shortdescription;
                lblLongDescription.Text = product.Longdescription;
                lblPrice.Text = String.Format("{0:c}",product.Price);
                lblTax.Text = String.Format(LocalizeString("lblTax.Text"), product.Tax);
                imgProduct.ImageUrl = PortalSettings.HomeDirectory + product.Image;
        catch (Exception exc) 
            Exceptions.ProcessModuleLoadException(this, exc);
    #region Interfaces
    public ModuleActionCollection ModuleActions
            ModuleActionCollection Actions = new ModuleActionCollection();
            Actions.Add(GetNextActionID(), Localization.GetString("EditProduct.Action", LocalResourceFile), ModuleActionType.EditContent, "", "edit.gif", EditUrl(), false, SecurityAccessLevel.Edit, true, false);
            return Actions;

All we have to do here is instantiating the Controller (BBLanguagePatternController class was built by the project template), retrieve the corresponding ProductLocInfo object (this is the view data for the current UI language and the current moduleId, created with the help of the CRUD-Generator (see first blog part), or coded manually)

public ProductLocInfo GetProductLoc(int moduleId, string language)
    using (IDataContext context = DataContext.Instance())
        var repository = context.GetRepository<ProductLocInfo>();
        return repository.Find("WHERE ModuleId = @0 AND Language = @1", moduleId, language).FirstOrDefault();

Binding the labels to the ProductLocInfo properties is standard work and we are ready to go!

The Edit.ascx

This is the exciting part. First lets have a look at the UI:


So how is this done ? The ascx-code is fairly simple too. We need to register the Language editor control (and the dnn filepicker control for the image) and use them later in the code. To combine the Control with the data to edit, we reference our ProductLangInfo class (containing the LanguageEditor attributes) in the LanguageEditor tag (InternalType=”…”) :

<%@ Control language="C#" Inherits="Bitboxx.DNNModules.BBLanguagePattern.Edit" AutoEventWireup="true" Codebehind="Edit.ascx.cs" %>
<%@ Register TagPrefix="dnn" TagName="Label" Src="~/controls/LabelControl.ascx" %>
<%@ Register TagPrefix="bb" TagName="LanguageEditor" Src="Controls/LanguageEditorControl.ascx" %>
<%@ Register TagPrefix="dnn" TagName="FilePickerUploader" Src="~/controls/filepickeruploader.ascx" %>
<div class="dnnForm bblanguagepattern-productedit dnnClear" id="bblanguagepattern-edit">
        <asp:HiddenField ID="hidProductId" runat="server" />
        <div class="dnnFormItem">
            <dnn:Label ID="lblImage" runat="server" ControlName="ctlImage"  Suffix=":"/>
            <dnn:FilePickerUploader ID="ctlImage" runat="server" Required="True" />
        <div class="dnnFormItem">
            <dnn:Label ID="lblPrice" runat="server" ControlName="txtPrice"  Suffix=":"/>
            <asp:TextBox ID="txtPrice" runat="server" MaxLength="12" />
        <div class="dnnFormItem">
            <dnn:Label ID="lblTax" runat="server" ControlName="txtTax"  Suffix=":"/>
            <asp:TextBox ID="txtTax" runat="server" MaxLength="10" />
        <bb:LanguageEditor ID="lngProduct" runat="server" InternalType="Bitboxx.DNNModules.BBLanguagePattern.ProductLangInfo" />
    <ul class="dnnActions dnnClear">
        <li><asp:LinkButton CssClass="dnnPrimaryAction" ID="cmdUpdate" runat="server" resourcekey="cmdUpdate" OnClick="cmdUpdate_Click" /></li>
        <li><asp:LinkButton CssClass="dnnSecondaryAction" ID="cmdCancel" runat="server" resourcekey="cmdCancel" OnClick="cmdCancel_Click" CausesValidation="false"/></li>

At Load event we retrieve the corresponding product and set the values for the textboxes, the filepicker and our LanguageEditor. (The controller method to read the ProductLangs and all other DAL2 methods could also be generated with CRUD-Generator or coded manually)

_product = Controller.GetProductLoc(ModuleId, CurrentLanguage);
if (_product != null)
    hidProductId.Value = _product.ProductId.ToString();
    if (!IsPostBack)
        ctlImage.FilePath = _product.Image;
        txtPrice.Text = _product.Price.ToString();
        txtTax.Text = _product.Tax;
        var dbLangs = new List<ILanguageEditorInfo>();
        foreach (ProductLangInfo productLang in Controller.GetProductLangs(_product.ProductId))
        lngProduct.Langs = dbLangs;

In cmdUpdate_Click we read out all values and save them back to the database:

int productId = Convert.ToInt32(hidProductId.Value);
ProductInfo product = new ProductInfo();
product.ProductId = productId;
product.Image = ctlImage.FilePath.Replace("//", "/");
product.Price = Convert.ToDecimal(txtPrice.Text.TrimEnd());
product.Tax = txtTax.Text.TrimEnd();
product.ModuleId = ModuleId;
if (productId > -1)
    productId = Controller.InsertProduct(product);
// Now lets update Language information
foreach (ProductLangInfo lang in lngProduct.Langs)
    lang.ProductId = productId;


With the LanguageEditor control, the help of the CRUD-Generator and the tuned PetaPoco tt files there are no more excuses to write only single language modules. Feel free to download the sample module, test it and ask questions if something is not working.

Download Sample project

Download CRUD Generator

Total: 24 Comment(s)
Michael T. Harwell
Get the dissertation writing service students look for these days with the prime focus being creating a well researched and lively content on any topic. https://paymetodoyourhomework.xyz/">Pay To Write My Test For Me
Monday, June 11, 2018 · reply ·
Michael T. Harwell
Great Information,it has lot for stuff which is informative.I will share the post with my friends. https://paymetodoyourhomework.xyz
Monday, June 11, 2018 · reply ·
Get information on your site - please continue to write more blog posts on this subject http://www.arbuk.co.uk">Tree Felling Oxford
Wednesday, July 4, 2018 · reply ·
You put a bet if the joined last scores of the two groups is finished or unde. http://www.betfaircasino.eu/
Saturday, July 7, 2018 · reply ·
The website is looking bit flashy and it catches the visitors eyes. Design is pretty simple and a good user friendly interface.
Saturday, July 7, 2018 · reply ·
Genuine gambling clubs must need to constrain their movement since they claim a place and the space is restricted. http://lp.nightrush.com/SE/sport100/?btag=a_8784b_625c_
Sunday, July 8, 2018 · reply ·
ryukin goldfish
I really love this post I will visit again to read your post in a very short time and I hope you will make more posts like this. 
Thursday, July 19, 2018 · reply ·
https://goldfisho.com/ I really love this post I will visit again to read your post in a very short time and I hope you will make more posts like this. 
Thursday, July 19, 2018 · reply ·
html color codes
I really love this post I will visit again to read your post in a very short time and I hope you will make more posts like this. https://htmlcolor-codes.com/
Monday, July 23, 2018 · reply ·
red nose pocket pitbull
Thanks for a very interesting blog. What else may I get that kind of info written in such a perfect approach? https://www.pocketpitbull.com/">red nose pocket pitbull
Wednesday, August 1, 2018 · reply ·
Togel Online
Incredible posting this is from you. I am really and truly thrilled to read this marvelous post. You've really impressed me today. I hope you'll continue to do so! http://www.mlaja.org
Thursday, August 2, 2018 · reply ·
This is really something new. I am sure that all of the above coding technique is new for people and they can easily tackle different coding issues using your solution. https://www.theacademicpapers.co.uk/assignment-writing-services-uk.php
Saturday, August 4, 2018 · reply ·
Sydney video production
This is only the data I am discovering all over the place. A debt of gratitude is in order for your website, I simply subscribe your online journal. This is a decent blog.. https://copestreetcrew.com.au
Saturday, August 4, 2018 · reply ·
Dissertation Help Uk
Great Info! I Recently Came Across Your Blog And Have Been Reading Along. I Thought I Would Leave My First Comment. I Don’t Know What To Say Except That I Have
Thursday, August 16, 2018 · reply ·
PHP programming Help
Thanx For Sharing Such Useful Post Keep It Up :) https://phphelponline.com
Thursday, August 16, 2018 · reply ·
I hope you will share such type of impressive contents again with us so that we can utilize it and get more advantage. pornmoviesjoy
Thursday, August 23, 2018 · reply ·
As demonstrated by the name the objective is to make a hand that signifies 21. Face cards are on the whole worth 10 focuses and an ace can be worth 1 or 11 focuses, which is dependent upon you. https://www.playojo.com/en/?aname=osmedia99#!registrationModal">play ojo
Tuesday, August 28, 2018 · reply ·
MathCad Online Assignment Help
This Is Really Great Work. Thank You For Sharing Such A Useful Information Here In The Blog. http://www.mathcadhelp.com
Tuesday, September 11, 2018 · reply ·
SAS Project Online Help
I Loved The Way You Discuss The Topic Great Work Thanks For The Share Your Informative Post. https://www.sashelponline.com
Tuesday, September 11, 2018 · reply ·
Je vous remercie de l'information! Je cherchais et ne pouvait pas trouver. Vous me aidé! http://percentagecalculator.wiki/
Tuesday, October 2, 2018 · reply ·
voyance gratuite en ligne
This is extremely fascinating substance! I have completely delighted in perusing your focuses and have reached the conclusion that you are right about a hefty portion of them. You are extraordinary.  http://voyance-amour-eternel.com/">voyance gratuite en ligne
Sunday, October 7, 2018 · reply ·
listen to music
This is an awesome blog. You provide very useful data. https://medium.com/@mahfug50/enlighten-your-mood-by-listening-to-audio-%C7%80-mp3-songs-d47cc2a9683 listen to music
Sunday, October 21, 2018 · reply ·
Thanks For sharing this information. It’s Nice..!!! https://games.lol/
Thursday, October 25, 2018 · reply ·
its great post and help me alot. please keep continue posting articles on your great site. https://games.lol/
Thursday, October 25, 2018 · reply ·


Torsten WeggenMy name is Torsten Weggen and I am CEO of indisoftware GmbH in Hanover, Germany. I'm into DNN since 2008. Before this, I did a lot of desktop stuff mainly coded with Visual Foxpro (see http://www.auktionsbuddy.de). 

I'm programmer, husband, father + born in 1965.

Please feel free to contact me if you have questions.

Latest Posts

DNN module development with Angular 2+ (Part 7)
6/10/2018 1:43 PM | Torsten Weggen
DNN module development with AngularJS (Part 6)
12/16/2016 7:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 5)
12/16/2016 6:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 4)
12/16/2016 5:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 3)
12/16/2016 4:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 2)
12/16/2016 3:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 1)
12/15/2016 7:19 AM | Torsten Weggen
Blogging in DNN with Markdown Monster by Rick Strahl
11/27/2016 1:14 PM | Torsten Weggen
Creating a global token engine
11/18/2016 10:25 AM | Torsten Weggen
DnnImagehandler - Hot or not ?
2/21/2015 11:52 PM | Torsten Weggen

My Twitter

Torsten Weggen 10/11/2018

Evernote or OneDrive user and love #markdown #markdownmonster ? Take a look at Joplin! https://t.co/4bzkiD6CHn

Torsten Weggen 9/14/2018

@booler @kristofclaes @RickStrahl until a phone is not able to be a complete replacement for my laptop (with a dock) thats ridiculous

Torsten Weggen 9/12/2018

Just finished the course "Angular RxJs Reactive Programming & FREE Ebook" https://t.co/2H0Rk3NuRB via @udemy