前言

这段时间在研究多语言的实现,就找了NopCommerce这个开源项目来研究了一下,并把自己对这个项目的粗浅认识与大家分享一下。

挺碰巧的是昨天收到了NopCommerce 3.90 发布测试版的邮件:

电脑培训,计算机培训,平面设计培训,网页设计培训,美工培训,Web培训,Web前端开发培训

不啰嗦了,开始正题了!

其实对于Nop的多语言,最主要的元素有下面两个:

  • WebWorkContext(IWorkContext的实现类)

  • LocalizationService(ILocalizationService的实现类)

其他相关的元素可以说都是在这两个的基础上体现价值的。

下面先来介绍一下WebWorkContext的WorkingLanguage属性,这个是贯穿整个应用的,所以必须要先从这个讲起。

WorkingLanguage

WebWorkContext中对多语言来说最为重要的一个属性就是WorkingLanguage,它决定了我们当前浏览页面所采用的是那种语言。

每次打开一个页面,包括切换语言时,都是读取这个WorkingLanguage的值。当然在读的时候,也做了不少操作:

  1. 从当前上下文中的_cachedLanguage变量是否有值,有就直接读取了这个值。

  2. GenericAttribute表中查询当前用户的语言ID,这张表中的字段Key对应的值是LanguageId时,就表明是某个用户当前正在使用的语言ID。

  3. Language表中查询出语言信息(当前店铺->当前店铺默认->当前店铺的第一个->所有语言的第一个)

查询语言表时,首先查出店铺支持的所有语言,然后找到当前用户正在使用的语言ID,根据这两个条件组合得到的Language实体就是当前的WorkingLanguage。

如果说这两个条件的组合拿不到相应的语言实体,就会根据当前Store的默认语言ID(如下图所示)去找。

电脑培训,计算机培训,平面设计培训,网页设计培训,美工培训,Web培训,Web前端开发培训

如果根据Store的默认语言还是不能找到,就会取这个Store语言列表的第一个。

如果还是没有查找到相应的语言,那就不会根据Store去找语言,而是直接取所有发布语言中的第一个,这就要确保在数据库中必须存在一个初始化的语言。

初始化对任何一个系统都是必不可少的!!

下面是这个属性get具体的实现片段:

if (_cachedLanguage != null)    return _cachedLanguage;

Language detectedLanguage = null;if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled)
{    //get language from URL
    detectedLanguage = GetLanguageFromUrl();
}if (detectedLanguage == null && _localizationSettings.AutomaticallyDetectLanguage)
{    //get language from browser settings
    //but we do it only once
    if (!this.CurrentCustomer.GetAttribute<bool>(SystemCustomerAttributeNames.LanguageAutomaticallyDetected, 
        _genericAttributeService, _storeContext.CurrentStore.Id))
    {
        detectedLanguage = GetLanguageFromBrowserSettings();        if (detectedLanguage != null)
        {
            _genericAttributeService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.LanguageAutomaticallyDetected,                 true, _storeContext.CurrentStore.Id);
        }
    }
}if (detectedLanguage != null)
{    //the language is detected. now we need to save it
    if (this.CurrentCustomer.GetAttribute<int>(SystemCustomerAttributeNames.LanguageId,
        _genericAttributeService, _storeContext.CurrentStore.Id) != detectedLanguage.Id)
    {
        _genericAttributeService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.LanguageId,
            detectedLanguage.Id, _storeContext.CurrentStore.Id);
    }
}var allLanguages = _languageService.GetAllLanguages(storeId: _storeContext.CurrentStore.Id);//find current customer languagevar languageId = this.CurrentCustomer.GetAttribute<int>(SystemCustomerAttributeNames.LanguageId,
    _genericAttributeService, _storeContext.CurrentStore.Id);var language = allLanguages.FirstOrDefault(x => x.Id == languageId);if (language == null)
{    //it not found, then let's load the default currency for the current language (if specified)
    languageId = _storeContext.CurrentStore.DefaultLanguageId;
    language = allLanguages.FirstOrDefault(x => x.Id == languageId);
}if (language == null)
{    //it not specified, then return the first (filtered by current store) found one
    language = allLanguages.FirstOrDefault();
}if (language == null)
{    //it not specified, then return the first found one
    language = _languageService.GetAllLanguages().FirstOrDefault();
}//cache_cachedLanguage = language;return _cachedLanguage;

因为这里目前不涉及对这个属性的set操作,只有在切换语言的时候会涉及,所以set的内容会放到切换语言的小节说明。并且在大部分情况下,用到的都是get操作。

视图中常规的用法

来看看Nop中比较常规的用法:

我拿了BlogMonths.cshtml中的一小段代码做演示:

电脑培训,计算机培训,平面设计培训,网页设计培训,美工培训,Web培训,Web前端开发培训

在视图中,可以看到很多这样的写法,几乎每个cshtml文件都会有!

这里的T其实是一个delegate。这个delegate有2个输入参数,并最终返回一个LocalizedString对象。

比较经常的都是只用到了第一个参数。第一个参数就是对应 LocaleStringResource表中的ResourceName字段

可以把这个对应关系理解为一个key-value,就像用网上不少资料用资源文件处理多语言那样。

下图是在LocaleStringResource表中用Blog做模糊查询的示例结果:

电脑培训,计算机培训,平面设计培训,网页设计培训,美工培训,Web培训,Web前端开发培训

至于第二个参数怎么用,想想我们string.Format的用法就知道个所以然了。只要在ResourcesValue中存储一个带有占位符的字符串即可!

上图中也有部分ResourcesValue用到了这个占位符的写法。

其实我们看了它的实现会更加清晰的理解:

public Localizer T
{
    get
    {        if (_localizer == null)
        {            //null localizer            //_localizer = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args));            //default localizer            _localizer = (format, args) =>
                             {
                                 var resFormat = _localizationService.GetResource(format);                                 if (string.IsNullOrEmpty(resFormat))
                                 {                                     return new LocalizedString(format);
                                 }                                 return
                                     new LocalizedString((args == null || args.Length == 0)
                                                             ? resFormat
                                                             : string.Format(resFormat, args));
                             };
        }        return _localizer;
    }
}

此时可能大家会有个疑问,这里返回的是一个LocalizedString对象,并不是一个字符串,那么,它是怎么输出到页面并呈现到我们面前的呢??

最开始的时候我也迟疑了一下,因为源码在手,所以查看了一下类的定义:

public class LocalizedString : MarshalByRefObject, IHtmlString{}

看到这个类继承了IHtmlString接口,应该就知道个七七八八了!这个接口的ToHtmlString方法就是问题的本质所在!

当断点在LocalizedString实现的ToHtmlString方法时会发现,大部分都是走的这个方法,返回的内容也就是所谓键值对中的值。

其中还有部分是显式调用Text等其他属性的。

有兴趣深入了解这个接口的内容,可以去看看msdn上面相关的内容。

视图中强类型的使用

说起强类型,大家应该也不会陌生,毕竟大部分的MVC教程都会涉及。

在System.Web.Mvc.Html这个命名空间下,有不少静态类(如InputExtensions,SelectExtensions等)和静态方法(如TextBoxFor,PasswordFor等)。

其中这些静态方法中,以For结尾的都是归属于强类型。

看看它们的方法签名就知道了为什么叫强类型了。

public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression);

下面就来看看,Nop在多语言这一块是怎么个强类型法。

Nop在强类型这一块的就一个扩展:NopLabelFor

Nop只在Nop.Admin这个项目中用到这个扩展的,在Nop.Web是没有用到的。

在我个人看来,这一块的实现可以说是挺妙的!下面来看看它是怎么个妙法:

先来看看它的用法,既然是强类型的,就必然有两个方面,一个是View,一个是Model

View中的用法

@Html.NopLabelFor(model => model.Name)

Model的定义

[NopResourceDisplayName("Admin.Configuration.Languages.Fields.Name")]
[AllowHtml]public string Name { get; set; }

在View中的用法和其他强类型的写法并没有什么太大的区别!只是在Model定义的时候要加上一个Attribute做为标识

下面来看看它的实现,其实这个的实现主要涉及的相关类就只有两个:

  • 一个是视图的扩展-HtmlExtensions

  • 一个是模型相关的Attribute-NopResourceDisplayName

先来看一下NopResourceDisplayName的实现

public class NopResourceDisplayName : System.ComponentModel.DisplayNameAttribute, IModelAttribute
{    private string _resourceValue = string.Empty;    //private bool _resourceValueRetrived;

    public NopResourceDisplayName(string resourceKey)
        : base(resourceKey)
    {
        ResourceKey = resourceKey;
    }    public string ResourceKey { get; set; }    public override string DisplayName
    {        get
        {            //do not cache resources because it causes issues when you have multiple languages
            //if (!_resourceValueRetrived)
            //{
            var langId = EngineContext.Current.Resolve<IWorkContext>().WorkingLanguage.Id;
                _resourceValue = EngineContext.Current
                    .Resolve<ILocalizationService>()
                    .GetResource(ResourceKey, langId, true, ResourceKey);            //    _resourceValueRetrived = true;
            //}
            return _resourceValue;
        }
    }    public string Name
    {        get { return "NopResourceDisplayName"; }
    }
}

重写了DisplayNameAttribute的DisplayName ,这样在界面中展示的时候就会显示这个值 , 实现了IModelAttribute的Name。

其中DisplayName中是根据ResourcesKey去数据库中找到要显示的文字。Name是在HtmlExtensions中用于拿到对应的NopResourceDisplayName对象。

然后是扩展的具体写法:

public static MvcHtmlString NopLabelFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, bool displayHint = true)
{    var result = new StringBuilder();    var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);    var hintResource = string.Empty;    object value;    if (metadata.AdditionalValues.TryGetValue("NopResourceDisplayName", out value))
    {        var resourceDisplayName = value as NopResourceDisplayName;        if (resourceDisplayName != null && displayHint)
        {            var langId = EngineContext.Current.Resolve<IWorkContext>().WorkingLanguage.Id;
            hintResource = EngineContext.Current.Resolve<ILocalizationService>()
                .GetResource(resourceDisplayName.ResourceKey + ".Hint", langId);

            result.Append(helper.Hint(hintResource).ToHtmlString());
        }
    }
    result.Append(helper.LabelFor(expression, new { title = hintResource }));    return MvcHtmlString.Create(result.ToString());
}

这个扩展做的事其实也很简单,根据模型的NopResourceDisplayName这个Attribute去显示对应的信息。

不过要注意的是在这里还做了一个额外的操作:在文字的前面添加了一个小图标!

可以看到这句代码helper.Hint(hintResource).ToHtmlString(),它调用了另一个Html的扩展,这个扩展就只是创建了一个img标签。

最后的效果如下:

电脑培训,计算机培训,平面设计培训,网页设计培训,美工培训,Web培训,Web前端开发培训

这里还有一个关于验证相关的实现,这里的多语言实现与强类型的实现相类似,就不重复了,它的实现依赖于FluentValidation

模型Property的用法

上面提到的基本都是在页面上的操作的多语言,Nop中还有不少是直接在controller等地方将多语言的结果查出来赋值给对应的视图模型再呈现到界面上的!这一点十分感谢 Spraus 前辈的评论提醒!

下面以首页的Featured products为例补充说明一下这种用法。

foreach (var product in products)
{    var model = new ProductOverviewModel
    {
        Id = product.Id,
        Name = product.GetLocalized(x => x.Name),
        ShortDescription = product.GetLocalized(x => x.ShortDescription),
        FullDescription = product.GetLocalized(x => x.FullDescription),        //...
    };    //other code}

通过上面的代码片段,可以看出,它也是用了一个泛型的扩展方法来实现的。这个扩展方法就是GetLocalized

大家应该已经发现这里的写法与我们前面提到的强类型写法有那么一点类似~~都是我们熟悉的lambda表达式。

有那么一点不同的是,这里的实现是借助了Linq的Expression。

var member = keySelector.Body as MemberExpression;var propInfo = member.Member as PropertyInfo;

TPropType result = default(TPropType);string resultStr = string.Empty;string localeKeyGroup = typeof(T).Name;string localeKey = propInfo.Name;if (languageId > 0)
{    //localized value
    if (loadLocalizedValue)
    {        var leService = EngineContext.Current.Resolve<ILocalizedEntityService>();
        resultStr = leService.GetLocalizedValue(languageId, entity.Id, localeKeyGroup, localeKey);        if (!String.IsNullOrEmpty(resultStr))
            result = CommonHelper.To<TPropType>(resultStr);
    }


作者:Catcher ( 黄文清 )

来源:http://catcher1994.cnblogs.com/

声明: 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果您发现博客中出现了错误,或者有更好的建议、想法,请及时与我联系!!如果想找我私下交流,可以私信或者加我QQ。

http://www.cnblogs.com/catcher1994/p/6445871.html