Find a post...

DNN-Connect Blogs

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.

3935

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

3931

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:

3934

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:

3937

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
    /// 
    /// Runs when the control is loaded
    /// 
    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();
        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:

3938

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();
        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

crossposted from my personal blog @ bitboxx.net

I'm developer. Born in 1965, grown up in a small village near Hamburg. Studied IT in Hamburg and moved to Hanover in the late 90's. Married 24 years now and having 2 childs. After developing projects several years in desktop and web applications in Visual Foxpro I changed in 2007 to dotnet / C# / ASP.NET & DotNetNuke. Since then I'm addicted to DNN and made it my main hobby and profession. My latest hobbyhorse is doing DNN with client site programming
Comment(s)

Hosting liberally provided by

Philipp Becker 6007 7
Geoff Barlow 542 4
DNN-Connect 431 6
Peter Donker 5050 30
Christopher Hammond 678 2
Olivier Jooris 417 1
Daniel Mettler 11988 88
Clint Patterson 1 1
Jos Richters 65 1
James Rosewell 326 2
Will Strohl 1543 27
Ernst Peter Tamminga 438 4
Barry Waluszko 2755 2
Declan Ward 436 1
Gifford Watkins 722 9
Torsten Weggen 2670 3