X
dnn.blog.
Back

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)
   BEGIN
      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)
   END
GO
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'{databaseOwner}[{objectQualifier}BBLanguagePattern_ProductLang]') and OBJECTPROPERTY(id, N'IsTable') = 1)
   BEGIN
      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
      )
   END
GO
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
GO
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
GO

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
{
    [TableName("BBLanguagePattern_Product")]
    [PrimaryKey("ProductId")]
	[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; }
    }
	[Serializable]
    [TableName("BBLanguagePattern_ProductLang")]
	[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; }
    }
    [TableName("BBLanguagePattern_ProductLoc")]
	[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:

View.ascx

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"/>
    </div>
</div>

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)
    {
        try
        {
            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);
        }
    }
    #endregion
    #region Interfaces
    public ModuleActionCollection ModuleActions
    {
        get
        {
            ModuleActionCollection Actions = new ModuleActionCollection();
            Actions.Add(GetNextActionID(), Localization.GetString("EditProduct.Action", LocalResourceFile), ModuleActionType.EditContent, "", "edit.gif", EditUrl(), false, SecurityAccessLevel.Edit, true, false);
            return Actions;
        }
    }
    #endregion
}

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:

Edit.ascx

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">
    <fieldset>
        <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>
        <div class="dnnFormItem">
            <dnn:Label ID="lblPrice" runat="server" ControlName="txtPrice"  Suffix=":"/>
            <asp:TextBox ID="txtPrice" runat="server" MaxLength="12" />
        </div>
        <div class="dnnFormItem">
            <dnn:Label ID="lblTax" runat="server" ControlName="txtTax"  Suffix=":"/>
            <asp:TextBox ID="txtTax" runat="server" MaxLength="10" />
        </div>
        <bb:LanguageEditor ID="lngProduct" runat="server" InternalType="Bitboxx.DNNModules.BBLanguagePattern.ProductLangInfo" />
    </fieldset>
    <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>
    </ul>
</div>

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>();
        lngProduct.Langs.Clear();
        foreach (ProductLangInfo productLang in Controller.GetProductLangs(_product.ProductId))
        {
            dbLangs.Add(productLang);
        }
        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)
    Controller.UpdateProduct(product);
else
    productId = Controller.InsertProduct(product);
// Now lets update Language information
lngProduct.UpdateLangs();
Controller.DeleteProductLangs(productId);
foreach (ProductLangInfo lang in lngProduct.Langs)
{
    lang.ProductId = productId;
    Controller.InsertProductLang(lang);
}
Response.Redirect(Globals.NavigateURL(TabId),true);

Summary

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

Back
Total: 0 Comment(s)

about.me.

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 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
Rapid Module Development Part 2 - The multilanguage thing…
4/7/2014 7:32 PM | Torsten Weggen

My Twitter

Torsten Weggen 7/22/2017

RT @CBPSC: Ventrian modules now free on Github! https://t.co/aKLeZawVHN #DNNCMS https://t.co/x6mhie382I

Torsten Weggen 5/24/2017

RT @WaldkauzFolk: ++ Waldkauz goes W:O:A 2017 ++ Der Wahnsinn - Wir sind in diesem Jahr beim großartigen Wacken Open Air mit... https://t.…

Torsten Weggen 4/25/2017

RT @WaldkauzFolk: "A magical pagan folk release getting a lot of inspiration of myth, legends and tales from countries all over... https://…