[译]:Orchard扩展——本地化提供程序使用

返回目录索引
原文链接:Using the Localization Providers
译文链接:Orchard扩展——本地化提供程序使用

Orchard利用一个简单的API来支持本地化,其将输入的默认语言字符串(en-us)作为本地化数据的主键,然后返回当前语言环境最准确的本地化版本。

在Razor视图引擎(.cshtml)中使用T()

简单示例 —— 转化后的字符串将会返回输出

@T("This was a triumph!")

简单字符串时使用上述内容。

格式化用户数据

有时,数据需要注入到本地化字符串中。

请勿使用字符串拼接,因为数据的位置在不同的语言中可能处于不同的位置:

// 不好的用法:
@T("You have ") + Model.SmsCredits + @T(" credits left.")

应该使用参数化格式字符串:

// 推荐用法:
@T("You have {0} credits left", Model.SmsCredits)

数据编码

请注意,参数应在添加之前进行编码。例如,如果以下示例的noteText中包含<b>huge</b>:

@T("I'm writing a note here: {0}.", noteText)

这样,默认语言环境输出时,用户将看到标记:

I'm writing a note here: <b>huge</b> success. 

在99%的情况下,这或许是你需要的。这种自动编码可以保护你免受黑客的注入攻击。

但在极少数情况下,你对你所做的事很了解,并且希望注入未编码的字符串,这是你可以执行以下操作:

@T("I'm writing a note here: {0}.", new HtmlString(noteText))

它的最终结果将是:

“I’m writing a note here: huge success.”

注意:实现IHtmlString类型的任何对象都将注入未编码,因为我们假设它已经正确编码。这就是使用new HtmlString(noteText)的功能。
上述编码未编码比较拗口,未找到合适翻译,其实际表述就是:未编码 —— 将按原样显示,编码 —— 某些内容会进行转换。

例如,如果你进行如下处理:

@T("{0}: We do what we must because {1}",
    Html.ItemDisplayLink(apertureScienceContentItem),
    justification)

这样,操作链接将不会编码,并且按预期显示,而justification字符串会编码。

还有需要注意的是,格式字符串本身是被认为安全的,因为它是由模块作者提供,所以下面内容将如预期的一样显示:

@T("It's <em>hard</em> to overstate my <strong>{0}</strong>",
    emotion)

如果emotion中包含"<satisfaction>“, 则其结果字符串为"It’s hard to overstate my <satisfaction>”.

注入没字符串值

基础值类型在格式化之前不是html编码的,并且当前的语言文化将会用于对它们进行格式化:

@T("when {0} qty {1:#,##0.00} unit price {2:C}", _clock.UtcNow, 5.782, 87)

复数

资源字符串(如 {0} comment{0} comments)的复数是比较棘手的,因为不同语言之间存在差异,其中包含复数的规则或者你需要多少字符串来应对所有情况。尽管Orchard尚未实现所有可能的情况,但其API已经准备在以后支持它们。

如果一个字符串需要进行复数处理,请为默认语言提供两个字符串,并将复数参数作为第一个格式化参数:

@T.Plural("1 Comment", "{0} Comments", commentCount)
@T.Plural("Deleted 1 item of type {1}", "Deleted {0} items of type {1}",
    deleteCount, contentType)

在单数字符串中使用1,以此可以为翻译者提供更好的上下文(如上所示)。

复数参数必须是整数。

不要再视图中使用自定义逻辑来决定字符串的内容,因为不同的文化下,其视图逻辑可能会不同。

通过代码使用T()

使用依赖 注入设置

你可以在代码(如驱动类、服务类以及其他类)中使用T()本地化辅助方法。

通过向你的类中添加公共属性,Orchard可以检测它,并利用Autofac的属性注入功能来使用当前的Localizer配置你的方法。

其过程如下:

  1. 向你的类中添加公共属性:

     public Localizer T { get; set; }
    

技术上,你可以使用任何变量名,并不一定要用T作为属性名,但在此约定使用T

  1. 在构造函数中,为T属性赋一个默认值:

     protected YourClassName() {
     T = NullLocalizer.Instance;
     }
    

这是为了防止在Autofac配置好localizer之前,由于没有默认localizer而导致应用结束。

利用继承自接口IDependency的Component类来自动获取T()

Orchard中提供了一个抽象类——Component,它定义Orchard.Framework项目的IDependency.cs文件中:

public abstract class Component : IDependency {
    protected Component() {
        Logger = NullLogger.Instance;
        T = NullLocalizer.Instance;
    }

    public ILogger Logger { get; set; }
    public Localizer T { get; set; }
}

如果你的类实现了IDependency接口,那么你可以改为继承Component。这样,你的类就可以自动含有方法T()的声明和初始化。

在配置T()时使用它的属性

你可以再次查看上面的Razor示例来使用它。

唯一的不同是,当你通过代码使用localizer时,你不需要使用Razor的@符号来表示代码块的开始。

例如,在Razor中,示例如下:

@T("You have {0} credits left", Model.SmsCredits)

而在代码中处理的示例如下:

T("You have {0} credits left", Model.SmsCredits)

在ASPX视图引擎(.aspx)中使用T()

其使用方法几乎与Razor方式一样,只需将@表达式格式替换为<%: 表达式 %>格式。

如简单使用,将:

@T("This was a triumph!")

替换为:

<%: T("This was a triumph!") %>

其他复杂使用以此类推。

关于<%= %><%: %>

<%: %>适用于所有情况,因为它会自动处理编码。请勿使用<%= %>。如果你确实需要注入未编码的字符串,请使用HtmlString处理,同时继续使用<%: %>


译:奇葩史

WPF快速入门——布局介绍

为什么要布局

布局和房子的格局具有着相似的概念,都是为了提供更好的用户体验而进行 设计。下面我们讲讲几个常用的布局面板

布局面板

WPF中有许多布局面板,下面将会介绍几个常用的布局面板,其中最常用的应该是Grid,StackPanel和DockPanel三大布局面板。

Grid和UniformGrid

Grid面板在WPF界面开发中,应该是使用最多的布局面板了。其功能为网格布局,下面以一个例子来解释一下其功能:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="2*"/>
        <ColumnDefinition Width="3*"/>
    </Grid.ColumnDefinitions>
</Grid>

在本示例中,Grid布局为3行3列,其中行高采用了等分,列宽比例为1:2:3。其中行高和列宽都用到了*表示,这里也可以写具体的数值,或者填写auto*auto都是一种自适应的布局,不同之处在于:*可以说是对于外部的自适应 —— 自动填满;而auto可以说是对于内部的自适应 —— 最小空间占用。

在布局中处理好行列后,就可以向里面添加控件了,在使用控件时,需要为控件设置在哪一行哪一列(如,第一行第一列[从0开始]:Grid.Row="1" Grid.Column="1")。

与Grid非常相似的一个面板就是UniformGrid了。虽然UniformGrid不是在Grid上继承下来的,但是在使用上可以说UniformGrid是Grid的一个特殊化处理。下面列举几点它们的区别:

  • Grid需要通过RowDefinitions和ColumnDefinition来定义行和列的个数,UniformGrid只需要通过属性Rows和Columns设置
  • Grid行高列宽手动设置,UniformGrid中完全等分。
  • Grid中的控件必须手动指定行列,否则只能在第0行第0列;UniformGrid中自动排列。
  • Grid中某行某列可以放多个控件;而在UniformGrid中必须嵌套布局面板才可以。

Grid和UniformGrid还有其他的不同,此处不再赘述。而且相对来说,UniformGrid的使用范围较小,最有用的使用情况就是业务表单录入类的界面 —— 需要大量的网格。

WrapPanel和StackPanel

此处之所以将WrapPanel和StackPanel放在一起,是因为他们有一定的相似之处。他们关系和上面的Grid和UniformGrid差不多,没有继承关系,但是StackPanel又有些像WrapPanel的特殊化处理。WrapPanel——流式布局,就像我们的写字一样,一行一行的,或者一列一列的;而StackPanel——堆栈式布局,就像一个不会换行/列的WrapPanel。下面分别说一下它们的使用场景:

WrapPanel除了上面说的写字以外,最常见的应该就是日历了,然后就是各大电商/视频网站中常会见到。而对于StackPanel,最熟悉的场景就是买票排队了,然后在应用中最常见的就是新闻列表、消息列表等(列表类内容)。如果你做的应用需要块状显示结果(多行多列),就用WrapPanel,如果需要条状展示结果(单行多列或多行单列),就用StackPanel。

在上述两个面板中,相对来说,StackPanel会使用的更加频繁一些。

DockPanel

DockPanel是三大最常用面板的最后一个,与前两个相比,使用量上会相对少些。但在我们的生活当中却是最常见的,比如你家里摆放物品时,常会需要哪个哪个靠墙边放。所以这个布局会出现在最不起眼的地方,但有时却是必不可少的。它的作用是让控件靠边(上下左右)停靠,软件应用中的菜单栏是一个比较典型的应用场景。

对于DockPanel还有一点需要注意的是:先到先得,就是先添加的控件先占用部分位置,然后后面添加的控件在剩余部分分割空间,不影响先前的控件 —— 这里控件的前后是指xaml文件中控件定义的位置先后。

Canvas及元素InkCanvas

Canvas是一个以坐标来确认控件位置的布局面板。在Canvas中的控件,需要以Canvas面板左上角为原点来设置坐标。相比之下,Canvas使用的会很少。只有在比较特殊的领域中,Canvas才有其展现的机会。而提到Canvas,还有一个比较特殊的元素InkCanvas,它与Canvas比较相似,但 它并不是布局面板,它直接继承自FrameworkElement;另外,InkCanvas主要用于手写 输入领域。

综合上述内容,Canvas的效率最高,当然它的功能也最弱;如果你去看它们的实现源码,会发现DockPanel和Canvas实现是比较简单的,所以效率也高,而Grid是最复杂的,所以它的效率相对是最低的。故在应用中选择最合适布局面板,有利于提高软件应用的性能;如果自己有能力,也可以自己实现Panel。
面板性能参考:Canvas略高于DockPanel,高于WrapPanel,高于StackPanel,高于Grid —— 由源代码复杂程度大概估算,未进行详细实测。

常与布局组合使用的控件

在使用布局面板时,常常会需要一些辅助的控件来实现一些特殊的功能,下面会介绍几个常见的辅助控件。

Border

Border是一个非常特殊的“控件”。它像是一个面板,但由不是;说它是个控件,它又不继承自Control,而是继承自Decorator。它常用于为面板添加边框。因为面板是不具有边框相关的属性的,如果需要在 布局中添加边框,就需要在相应的布局面板外侧添加Border,以此添加边框。而且Border不仅仅是可以添加边框,还可以处理一些圆角等处理,可以让软件应用更加圆滑。Border与布局面板还有一个主要的区别就是:

  • Border内只能包含一个元素(面板、控件等),所以,想要添加多个控件,就必须先添加一个面板,然后向面板中添加控件。

ScrollViewer

ScrollViewer也是一个常与布局组合使用的控件,与Border不同的是,它确实是一个控件(继承自Control)。用到ScrollView的原因是,布局面板本身是不提供滚动支持的,当内容超出布局面板外侧的大小限制时,我们就无法查看超出内容,此时就需要ScrollViewer来处理了。还有一点与Border相同的是,它里面也只能有一个元素,到添加多个,则必须嵌套布局面板。

GridSplitter

看到GridSplitter名字就应该 知道它是 和Grid组合 使用的,它是用于在运行期间调整界面布局大小的。最常见的就是在左侧列表,右侧具体内容的界面布局中,需要看左侧列表的内容,但列表宽度又太小,此时,添加GridSplitter就可以让用户自己拖动来扩大列表的宽度,以便于查看更多内容。

附加属性

附加属性简单点说就是,这个属性不是元素(如控件)自己的,是别人给它的。最常见的使用场景就是上面的布局面板了,如Grid中,内部控件会有Grid.Column的 属性,这就是一个附加属性;还有如DockPanel中,内部控件会有DockPanel.Dock属性,这也是一个附加属性。当然,附加属性并不是外侧面板给它的,只是上面提到的两个属性,只有在相应的面板中才有效而已。

或者,在往深了说,附加属性不是真正的属性,它是一个方法调用,只是xaml中封装了方法调用,让它看起来是个属性的样子。而在个人所遇到的情况中,自定义附加属性的情况真的是很少很少 ———— 最典型的一次是使用PasswordBox,由于PasswordBox的Password不支持绑定,所以使用到了附加属性来处理,其中内容涉及到了依赖属性,此处不做详细介绍,仅做举例。

上述内容继承结构

WPF快速入门——Hello,World和XAML介绍

个人开发环境 —— 按个人喜好选择:

  • IDE:Visual Studio Community 2015 —— 愿意尝鲜的可以试下VS2017
  • System:Windows 8.1

Hello World

尽管hello,world的例子已经用烂了,但你不可否认它所带来的无限魅力。如果你去查找hello,world起源,会发现许多有趣的事。所以本文仍以hello,world为例开始我们的WPF学习之旅。

创建第一个WPF项目

操作步骤:打开Visual Studio —— 依次点击文件-新建-项目 —— 在弹出的新建项目对话框中的索引目录区域选择已安装-模板-Visual C#-Windows —— 选中对话框列表项中的WPF 应用程序 —— 在对话框下方的名称中输入名称Hello_World —— 点击确定完成项目创建。

创建Hello,World项目

项目结构说明

  • 解决方案Hello_World —— VS解决方案,可以用于同时管理多个项目
    • Hello_World —— 项目,即刚刚新建的Hello,World项目
      • Properties —— 项目属性相关内容设置,包括程序集信息、一些资源等内容
      • 引用 —— 项目中需要的依赖程序集
      • App.config —— 项目配置,在Hello,World项目中可有可无。
      • App.xaml —— 含两个文件App.xaml和App.xaml.cs;此部分功能主要是设置应用的启动窗体及一些样式资源的引用,常用于主窗体显示前的一些准备工作设置。
      • MainWindow.xaml —— 含有两个文件MainWindow.xaml和MainWindow.xaml.cs;.xaml文件为界面设计文件,.cs文件为后台代码文件

Tips:全局异常处理

为了避免应用出现异常后直接退出,我们常会需要添加一个全局的异常捕获处理。处理方法为,在App.xaml.cs中重载OnStartup方法,为DispatcherUnhandleException事件添加处理内容。代码如下:

protected override void OnStartup(StartupEventArgs e)
{
    this.DispatcherUnhandledException += App_DispatcherUnhandledException;
    
    base.OnStartup(e);
}

private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
    //throw new NotImplementedException();
    e.Handled = true;
}

注意:在事件处理中添加你自己的实现内容,可以添加对话框提示等。如果有些异常处理完后,不需要应用退出,则将e.Handled设置为true;如果此处为致命错误,必须退出应用软件,则e.Handled保留原来的false。

XAML基础介绍

XAML文件是以.xaml为后缀的XML文件。其是为了简化UI的创建过程而推出的。它不仅仅在WPF中应用,在微软后续的UMP开发中也是利用此技术进行UI处理。

注:XAML读音为“zammel”

命名空间说明

在XAML文件中,命名空间利用xmlns(xml namespace)来引入。命名空间引入基本格式为:

xmlns:别名="命名空间"

其中默认命名空间可以省略“冒号”和“别名”,另,此处引用的命名空间与cs代码中的还有所不同,cs代码中只能一个空间一个空间引入,而此处的空间可以是多个命名空间的集合 —— URL表示,当然也可以一个一个空间引入。

类对应关系

在我们的示例中MainWindow.xaml根节点Window上有一个属性设置“x:Class”,此属性设置了其所对应的C#代码类。而在我们的MainWindow.xaml.cs的定义中,可以看到MainWindow类含有partial(分部类)修饰符。其实在最终的生成结果中,他们是同一个类,我们可以在obj\Debug目录下看到 MainWindow.g.i.cs文件,它是在生成过程中的一个中间状态,从其内容结构上可以看出它也是MainWindow的一个分部类。如果将XAML文件中的类名修改,程序生成过程就会出现错误,因为MainWindow构造函数 中 的InitializeComponent()方法是xaml文件生成出来的。

注:分部类 —— 就是让你可以将一个类的内容定义到多个文件。—— 据我所知,这应该是.NET中特有的设计 。

基本属性及事件

属性设置其实和在cs代码中设置一样,仅仅是因为换了种写法,封装了一些处理而已。

简单的属性设置

简单属性设置,即可以用一个字符串表示的属性设置,如Name属性。还有就是一些位置Alignment的设置,如HorizontalAlignment可以设置为Center,在XAML文件中,Center是一个字符串,而在类中HorizontalAlignment是一个System.Windows.HorizontalAlignment的枚举类型的属性,所以此处就封装了字符串到Alignment枚举类型的一个转换。不过在我们界面设置的时候,是不太需要关注这一块的。

注:许多控件的Text或Content属性会与标签值(如<a>此处为标签值</a>)冲突 —— 即它们表示同一个属性,所以为了避免冲突,建议,可以直接设置Text或Content的值时,尽量设置在属性上设置,避免在标签中设置属性值。

// 建议
<TextBox Text="hello"></TextBox>
<Button Content="hello"></Button>

// 不建议
<TextBox >hello</TextBox>
<Button >hello</Button>

虽然效果一样,但看得不爽 —— 强迫症。如下方式也是可以的,主要用于复杂的属性设置:

<TextBox >
    <TextBox.Text>hello</TextBox.Text>
</TextBox>
<Button >
    <Button.Content>hello</Button.Content>
</Button>

复杂属性设置

复杂属性的设置表现在无法用一个字符串表示的情况,如颜色背景设置,但我们是单色的情况时,可以直接用一个字符串表示,但是当需要设置渐变色时,就无法一个值表示,所以需要多行语句来表示。

以下为背景色的渐变色设置,其位置变化为默认值,从左上角到右下角渐变:

<Button >
    <Button.Content>hello</Button.Content>
    <Button.Background>
        <LinearGradientBrush>
            <LinearGradientBrush.GradientStops>
                <GradientStop Offset="0" Color="Red"></GradientStop>
                <GradientStop Offset="1" Color="Blue"></GradientStop>
            </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>
    </Button.Background>
</Button>

还有一些属性相关内容,将在后面介绍,如附加属性,资源设置等。

添加事件

事件的添加有两种方式,一种是直接在XAML文件中和写属性一样添加 —— 利用自动创建,在后台代码中创建事件的实现。还有一种是在后台代码手动实现。写法纯粹看个人喜好了。

当然,后台代码手动实现的自由度更高一些,如某些事件的注册可能需要在界面加载完后注册会更好一些。个人是推荐在后台代码自己手动加,虽然写的时候会麻烦些,但可以自己整理的比较清晰一些。

Tips:学WPF的好处

  • 就本节内容而言,WPF界面设计采用xml格式,以后以此基础学习网页设计和Android开发会相对简单一些。

MEF基础指南——MEF导入导出

文章缘起

要说写MEF中的导入导出,我想没有哪一篇文章可以比得上微软的官方文档了(至少是我看到的里面没有)。
个人强烈建议看MEF导入导出的人去看微软官方文档,文档地址:

个人喜欢用 Microsoft Help查看器(Help Viewer) 离线查看,如果你也喜欢这样,你可以直接在里面搜索MEF,找到特性化编程模型概述即可。

既然微软官方文档已经很完善了,那本文自然不会在写一遍,故本文更倾向与一篇快速索引参考文。

快速参考

导入导出基础

  1. 无参数导入导出,默认使用当前类或接口,示例如下:

示例中最后a有效,而ia无效;

    public interface IA { }

    /// <summary>
    /// 等价于 [Export(typeof(A))]
    /// </summary>
    [Export]
    public class A : IA { }

    public class UseA
    {
        /// <summary>
        /// 等价于 [Import(typeof(A))]
        /// </summary>
        [Import]
        public A a { get; set; }

        /// <summary>
        /// 等价于 [Import(typeof(IA))]
        /// </summary>
        [Import]
        public IA ia { get; set; }
    }
  1. 含参数导入导出,要求类型和协定名完全匹配,未指定类型时,同上,使用当前声明的类或接口。

如下示例中,最终ib有效 ,而b是无效的。

    public interface IB { }

    [Export(typeof(IB))]
    public class B : IB { }

    public class UseB
    {
        [Import]
        public B b { get; set; }

        [Import]
        public IB ib { get; set; }
    }
  1. 延迟导入及导入多个;其中匹配类型时,均使用繁星内的类型。

在如下示例中,三个导入均有效。其中ImportMany主要用于多个具有相同协定名和类型的导出,如你有两个类使用了相同的类型和协定,用Import是会报错的,此时可以用ImportMany来处理。

    public interface IC { }

    [Export(typeof(IC))]
    public class C : IC { }

    public class UseC
    {
        [Import]
        public Lazy<IC> ic { get; set; }

        [ImportMany]
        public IEnumerable<IC> iclist { get; set; }

        [ImportMany]
        public IEnumerable<Lazy<IC>> icllist { get; set; }
    }

元数据

元数据可以简单理解为:针对具有相同协定和类型的导出,添加元数据来区分它们。

继承

在MEF中使用InheritedExport来设置继承导出,在继承结构中,导出的匹配类型为父类/接口的类型,示例如下:

在如下代码中,最后id是有效的,d是无效的,即类型D的导出继承ID,使用的类型为ID。

    [InheritedExport]
    public interface ID { }

    public class D : ID { }

    public class UseD
    {
        [Import]
        public D d { get; set; }

        [Import]
        public ID id { get; set; }
    }

结语

本文主要列出了最常用的导入导出参考,详细信息及更多扩展内容还是建议看微软官方文档:特性化编程模型概述 (MEF)


奇葩史

[译]:Orchard扩展——Orchard的工作原理

返回目录索引
原文链接:How Orchard Works
译文链接:Orchard扩展——Orchard的工作原理
本文为纯文字内容,稍显枯燥,不过若能全篇理一遍,对Orchard的整体架构技术可以有个比较好的了解。
另,本文有些内容个人看的时候还是一头雾水,有些地方翻译会比较晦涩。

构建一个web cms与常规的web应用有所不同,它更像是构建一个应用容器。当设计一个这类系统,我们需要把可扩展性作为首要功能特性。这是一个的挑战,因为开放式的架构会需要有很好的扩展性,但这可能需要降低应用的可用性来满足:系统中所有内容都需要能够和未来未知的模块进行组合,包括在UI层。将所有不互通的小部件组织为一个连贯的整体就是所谓的Orchard。

本文讲述了组成Orchard所做的架构选择,以及它是如何解决在提供灵活性的同时,又保证了良好的用户体验这一问题。

网站架构

Modules
Core
Orchard Framework
ASP.NET MVC NHibernate Autofac Castle
.NET ASP.NET
IIS or Windows Azure

Orchard基础

Orchard CMS是建立在现有框架和库之上的。以下是其中最基本的几个:

  • ASP.NET MVC: ASP.NET MVC是一个现代的Web开发框架 —— 它提倡业务分离。
  • NHibernate: NHibernate是一个对象关系映射工具。它用于处理Orchard内容项的持久化数据库存储,并在进行模块开发时,极大简化数据模型,以便于我们无需关注数据的持久化。你可以通过查看任何一个核心内容类型的源代码来查看示例,例如Pages。
  • Autofac: Autofac是一个IoC容器。Orchard大量使用了依赖注入。通过实现IDependency接口(或继承自IDependency的特定接口——如标记接口)来创建一个可注入的Orchard依赖就项写一个类一样简单;而使用依赖就像使用正确类型的构造参数一样简单。注入的依赖项的使用范围和生命周期则由Orchard框架进行管理。你可以通过查看IAuthorizationService, RolesBasedAuthorizationService 和 XmlRpcHandler的代码来查看示例。
  • Castle Dynamic Proxy: 我们使用Castle来进行动态代理生成。

Orchard应用及框架是在这些基础框架之上建立额外的抽象层。Orchard中有许多方法来实现具体功能内容,且不一定需要知道NHibernate, Castle, 或 Autofac。

Orchard框架

Orchard框架是Orchard中最低层的东西。它包含了应用程序的引擎以及不能继续拆分为模块的部分。那些东西通常是最基本的模块都要依赖的。你可以把它当作Orchard的基础类库。

Orchard启动

关于本节中“租户(tenant)”一词,网络未找到比较好的解释,仅在维基百科中的多租户技术中提到此词的解释:
租户(tenant)是指使用系统或电脑运算资源的客户。
多租户概念维基百科链接 —— 墙外链接

当Orchard web应用启动时,一个Orchard主机服务将会被创建。其主机服务为一个应用程序域级别的单例。

然后,主机服务会使用ShellContextFactory来为当前租户获取一个Shell。租户是应用程序的实例,它被拆分到了用户操作级别,但是它们还运行在同一个应用程序域 —— 提高网站的密度。Shell在租户级别是一个单例,或者可以说它实际上代表租户。它在租户级别有效的隔离了对象,并且保证了模块的编程模型与多租户无关。

shell一旦创建,将会从ExtensionManager中获取可用扩展列表(即模块和主题)。其默认实现是通过扫描模块和主题目录来获取扩展内容。

与此同时,shell还会从ShellSettingManager中获取有关租户的设置列表。其默认实现是在App_Data中对应的子文件夹中获取设置,但是我们可以另外实现来从其它不同的位置获取。例如,我们有一个Azure实现,它用来存储blob数据,因为在那种情况下,App_Data可写是不安全的。

之后,shell会获取组策略对象,并使用它依据当前主机服务的可用扩展列表和当前租户的设置来准备IoC容器。在此获得的结果并不是一个shell的IoC容器,它是一个ShellBlueprint —— 依赖、控制器和记录蓝图的一个列表。

至此,ShellSetting(每个租户)的列表和ShellBluePrint将会放入ShellContainerFactory。利用CreateContainer来获取一个ILifetimeScope —— 基本上可以在租户级别范围使用IoC容器,这样模块可以在不需要处理具体事宜的情况下,获取当前租户的作用域中注入的依赖。

依赖注入

在Orchard中,标准的创建注入依赖的方式是:先创建一个派生自IDependency(或者是它的派生接口)的接口,然后再实现这个接口。而在使用时,可以在你的构造函数中使用接口类型的参数。Orchard应用框架会发现所有依赖项,然后依据需要进行实例化和注入实例。

Orchard中为注入提供了三个可行的范围,从中选择正确的接口派生即可:

  • 请求——Request: 为每个新的http请求创建的依赖实例,且一旦请求完成,实例就会销毁。通过继承接口IDependency来使用。此对象应当相对低代价的创建。
  • 对象——Object: 每次对象通过接口获取依赖时,都会创建一个新的实例。且实例从不共享。通过继承接口ITransientDependency来使用。此对象必须很低代价的创建。
  • Shell: 每个shell/租户只创建一个实例。它利用ISingletonDependency接口派生。只有需要在shell生存期中必须保持一个共同状态的对象使用此创建。

替换现有依赖

可以通过使用OrchardSuppressDependency特性装饰类来实现替换现有依赖。它会将完全限定类型名称替换为一个参数。

依赖关系排序

一些依赖项并不唯一,而是列表的一部分。例如,handlers是同时处于活动状态。在某些情况下,你将会需要修改这些依赖的使用顺序。这可以通过修改模块清单来实现 —— 使用功能中优先级(Priority)属性。下面为示例代码:

Features:
    Orchard.Widgets.PageLayerHinting:
        Name: Page Layer Hinting
        Description: ...
        Dependencies: Orchard.Widgets
        Category: Widget
        Priority: -1

ASP.NET MVC

Orchard是基于ASP.NET MVC建立的,但是为了添加事物,如主题与租户分离,它需要引入一个额外的层,这样,在设想中ASP.NET MVC端将按预期进行控制显示呈现,而在Orchard概念层中的Orchard端拆分事物。

例如,当请求一个特定视图时,我们的LayoutAwareViewEngine将会启动。严格来讲,它并不是一个新的视图引擎,因为它不关心实际的渲染,但是,它包含依据当前主题查找到正确视图的逻辑处理,然后他会将渲染的工作交给实际的视图引擎。

同样,我们还有路由提供程序、模型绑定器和控制器工厂,它们的工作是作为ASP.NET MVC的单一入口点,并将调用分配到下面合理范围的对象。

就以路由为例,我们会有n个路由提供程序(通常来自模块)和一个与ASP.NET MVC交互的路由发布程序。模型绑定器和控制器工厂也是一样的。

内容类型系统

在Orchard中,内容由一个真实的类型系统管理,此类型系统相对与.net类型系统在某些方面要更加丰富且更加动态。为了在Web CMS中提供更灵活的处理,类型必须在运行的时候组合,并反应出内容管理的关注点。

类型、部件和字段

Orchard可以处理任意的内容类型,包括那些由网站管理员以无代码方式动态创建的类型。这些内容类型是内容部件的整合,其中每个部件都要处理特定的问题。其原因是许多问题会跨越多个内容类型。

例如,一篇博文,一个产品展示,和一个视频剪辑可能都含有一个路由地址,以及评论和标签内容。因此,在Orchard中路由地址、评论和标签是作为单独的内容部件来处理。这样,评论管理模块就可以只开发以此,并可以应用到任意的内容类型上,包括评论模块开发者不了解的那些类型。

部件本身可以含有属性和内容字段。内容字段可以和部件一样重复使用:一个特定的字段类型可以用于多个部件和内容类型。部件和字段的不同之处在于他们操作的范围和语意区别。

字段相对于部件要更加细粒度。例如,字段类型可以描述一个电话号码或一个坐标,然而,一个部件通常用于描述一个完整的问题,如评论或标记。

但是它们最重要的不同是他们的含义:如果要实现是一个的关系,你需要写一个部件;如果要实现有一个的关系,你就需要写一个字段。

例如,一件衬衫是一个产品,它库存和价格。你不可能说一件衬衫有一个产品或者它是一个价格或库存。

从上面可以知道,衬衫它是一个内容类型,它将是由产品部件组成,然后产品部件由一个名为价格的money字段和一个名为SKU的字符串字段组成。

另一个不同点是,在每一个内容类型中,你只能有一个给定类型的部件 —— 这样是一个关系才有意义,而一个部件可以含有给定类型的任意数量的字段。还有另一种说法是,部件上的字段是字段类型从字符串到值的字典,而内容类型是部件类型(不含名称)的列表。

这提供了另一种选择部件和字段的方式:如果你认为在每个内容类型中,你的对象需要多个实例,它就需要是一个字段。

内容类型解析

正如我们看到的,一个内容类型是从内容部件构建出来的。在代码方面,内容部件通常与以下内容有关:

  • 一条记录 —— 部件数据的POCO对象表示。
  • 一个模型类 —— 实际的部件,由ContentPart<T>派生出来,其中T是记录类型。
  • 存储库 —— 存储库不需要模块作者实现,因为 Orchard会提供通用的使用方式。
  • 事件处理程序 —— 处理程序实现IContentHandler接口,它是一系列的事件处理程序,如OnCreated或OnSaved。基本上,它们与内容项的生命周期挂钩,它们用于处理多个任务。他们还可以通过内容项的构造函数参与到它们的实际构成。在ContentHandler基类中有一个筛选器集合,可以允许处理程序添加通用处理到内容类型中。
    例如,Orchard提供了一个StorageFilter —— 用它可以方便的声明一个内容类型的持久化怎么处理:只需要使用Filters.Add(StorageFilter.For(myPartRepository));,这样Orchard就会将来自myPartRepository的数据持久化存储到数据库。
    另一个示例是ActivatingFilter —— 负责将一个类型关联到实际的部件上:调用Filters.Add(new ActivatingFilter<BodyAspect>(BlogPostDriver.ContentType.Name));,这样就是向博文添加正文内容部分。
  • 驱动 —— 驱动程序是一种更友好、更特殊的处理程序(因此相对不灵活),并且它与特定的内容部件相关联(他们派生自ContentPartDriver<T>,T为内容部件类型)。另外,处理程序不一定要指定一个内容部件类型。驱动程序可以看作一个特殊部件的控制器。他们通常需要通过主题引擎来构建显示的形态。

内容管理器

在Orchard中,所有的内容都通过内容管理器对象访问 —— 这使得在你不知道内容类型的情况下,仍然可以使用内容。

内容管理器含有查询内容存储、版本内容和管理发布状态的方法。

事务

Orchard自动为每个HTTP请求创建一个事务。这意味着在请求期间发生的所有操作都是“ambient”事务的一部分。如果代码在请求期间中止事务,则那个事务里面所有的数据操作都将回滚。如果事务从未显示地取消,所有的操作会在请求结束时提交,而不需要显示提交。

请求的生命周期

在本节中,我们将以特定的博文请求作为示例。

当针对特定博文发出请求时,应用首先会查看由各个模块贡献的可用路由,然后找到与博客模块匹配的路由。之后,路由会解析请求到博文控制器项的action —— action会从内容管理器中查找博文。然后action会基于请求主对象来从内容管理器(调用BuildDisplay)中获取页面对象Model(POM) —— 即在内容管理器中检索博文。

博文有自己的控制器,但是并支持所有内容类型。例如,动态内容类型将通过Core Routable部分中更通用的ItemController来处理。ItemController的Display操作与博文控制器中的操作几乎一样:它通过slug从内容管理器中获取内容项,然后使用结果生成POM。

然后,布局视图引擎将依据当前的主题和使用的模型类型以及Orchard中约定的视图名称来解析出正确的视图。

在视图中,可以进行更多的动态形状创建,如区域定义。

实际渲染将有主题引擎完成,主题引擎会寻找正确的模板或形状方法,并按照出现的顺序递归地来渲染它在POM中遇到的每个形状。

部件

部件是含有部件内容元件和部件模型的内容类型。和其他内容类型一样,它们由元件和字段组成。这意味着他们可以使用与其他内容类型相同的版本和渲染逻辑来进行编辑。它们也 可以共享相同的构件块,这意味着任何已存在的内容元件都有可能自由的组合到部件中。

部件通过部件层添加到页面。层是部件集合。它具有一个名称和一个规则(用于控制哪些网站页面需要显示),以及一个部件列表和相关的区域布局、排序和设置。

附加到每个层的规则是用IronRuby表达式表示。这些表达式可以使用应用中的任何IruleProvider实现。Orchard中提供了两个内置实现:url和authenticated。

网站设置

在Orchard中,站点是一个内容项,这样使得它可以为模块链接其他附加元件。这也是模块如何可以进行网站设置。

网站设置针对每个租户。

事件总线

Orchard以及其模块是通过创建依赖关系的接口来公开扩展端口,这样之后,就可以通过注入方式实现他们。

向扩展端口插入功能,是通过实现接口,或实现具有相同名称及方法的接口来完成的。也就是说,Orchard在向扩展端口添加插件时,不需要严格的强类型接口对应,其不依赖于其定义的组件。

这仅仅是Orchard事件总线的一个实现。当扩展端口 调用注入的实现时,一个消息就会发布到事件总线上。监听事件总线的对象之一会将消息分配给对应的类的方法(从相应命名接口派生的类)。

命令

在Orchard网站中,许多操作都可以使用命令以及控制面板UI来处理。这些命令通过在ICommandHandler的派生类中的方法上添加CommandName特性装饰来将其公开。

Orchard命令行工具通过在运行期间,模拟网站环境并通过反射检查程序集来发现可用 的命令。这样命令运行的环境会尽可能的接近实际运行的站点。

搜索与索引

搜索和索引在默认情况下是使用Lucene实现,但其可以使用另一个索引引擎来替代默认实现。

缓存

Orchard中的缓存依赖于ASP.NET的缓存,但是我们通过调用Get方法公布了一个辅助API —— 通过ICache类型的依赖关系来使用。如果缓存还没有包含请求的记录,则可以通过Get获取一个键和一个函数来生成缓存记录值。

使用Orchard API处理缓存的主要优点是,它对于每个租户的工作是透明的。

文件系统

Orchard中的文件系统是抽象化的,因此存储处理可以指向一个物理文件系统,或者一个备用的存储(如Azure blob存储)—— 取决于具体环境。媒体模块是使用该抽象文件系统的模块实现示例。

用户及角色

在Orchard中,用户是作为内容项处理(虽然不是可路由的),这让配置模块变得容易(如为其扩展附加字段)。角色则是用户的一个内容部件。

权限

每个模块都可以公布一组权限,以及如何将这些权限默认分配给Orchard的默认角色。

任务

模块可以通过在IScheduledTaskManager类型的依赖中调用CreateTask来计划安排任务。然后通过实现IScheduledTaskHandler来执行任务。Process方法可以检查任务类型名称,以及决定是否执行处理它。

任务是运行一个单独的线程上,此线程来自ASP.NET线程池。

通知

模块可以通过获取INotifier的一个依赖并调用里面的一个方法来向控制面板界面显示消息。可以通过创建多个通知来作为请求的一部分。

本地化

应用及其模块的本地化是通过在调用T方法时传入字符串资源来实现的:@T("This string can be localized")。关于更多详细信息和准则,参阅原文:Using the Localization Providers。Orchard资源管理器会从应用中的特定位置的PO文件来加载本地化 资源字符串。

内容项的本地化是通过不同的机制来完成的:内容项的本地化版本是通过一个特定的部分链接到内容项 —— 它是物理上分离的内容项。

当前的语言文化环境是由文化管理器来决定的。默认实现是返回在网站设置中配置的语言,但是还有代替实现是可以从用户配置文件或浏览器设置中获取。

日志记录

日志记录是 通过ILogger类型的依赖来完成的。不同的实现可以将日志记录发送到各种不同的存储介质类型中。Orchard利用实现Castle.Core.Logging来实现日志记录。关于更多Castle.Core.Logging内容,详见:Castle.Core.Logging

Orchard核心

Orchard.Core程序集包含一组Orchard运行所必需的模块。其他模块将依赖于这些模块,且这些模块将始终可用以此保证稳妥地运行。

核心模块的示例是:订阅,导航或可路由。

模块

Orchard的默认分发包含一些内置模块,如博客或页面,当然,第三方的模块也在构建中。

模块只是一个带有用于扩展Orchard的manifest.txt文件的ASP.NET MVC区域。

一个模块通常要包含事件处理程序,内容类型和他们的默认呈现模板以及相应的管理界面。

模块可以在每次对csproj文件或csproj文件引用的文件进行改变时,从源代码进行动态编译。这可以让我们使用“记事本”风格进行开发,即不需要开发人员显示编译,甚至 是,不需要使用IDE,如Visual Studio。

模块必须放置在Modules文件夹中(Orchard.Web/Modules/MyModule),并且文件夹的名称必须与项目编译生成的dll名称匹配。因此,如果你有一个名为My.Custom.Models.csproj的自定义模块的项目,并且它编译后名为My.Custom.Module.dll,那么模块的顶级文件夹 必须命名为My.Custom.Module,即~/Modules/My.Custom.Module/

主题

主题是Orchard中的一个基本的设计原则,Orchard中生成的所有html都可以从主题中替换,包括模块生成的标记。其中规则定义了在主题文件的层次结构中,文件必须放在哪里。

Orchard中整个渲染机制是基于形状。主题引擎的工作是找到当前主题,然后确定主题渲染每个形状的最好方式是什么。每个形状可能有一个由模块定义的默认渲染。它可能是在视图文件夹中以模板方式定义,也可能是代码中以形状方法定义。默认渲染可能会被当前主题覆盖。主题会通过形状来创建自己的模板或者形状方法。

主题可以有一个父级,它可以让子主题以父主题为基础进行自己的特殊化处理或者修改处理。Orchard默认带有一个名为Theme Machine的基本主题,其可以用于作为父主题来使用。

主题可以包含于模块完全相同的代码:他们可以有自己的csproj文件,并可以利用动态编译。这让主题可以定义形状方法,还可以向管理界面公开任何可能有的设置。

当前主题的选择是由实现IThemeSelector的类来处理的,这些类可以返回主题的名称以及任何请求的优先级。这允许多个选择器来出来里主题的选择。Orchard默认带有四个IThemeSelector实现:

  • SiteThemeSelector —— 选择当前为最低优先级的租户或站点配置的主题。
  • AdminThemeSelector —— 当前URL是一个管理URL时,此实现接管并返回具有高优先级的管理主题。
  • PreviewThemeSelector —— 当前用户启动主题预览,则此主题覆盖网站当前主题和正在预览的主题。
  • SafeModeThemeSelector —— 当应用处于安全模式时,此主题是唯一可用的选择器,这通常 在安装过程中发生。它的优先级非常低。

主题选择器的示例可以是在当前useragent被识别为移动设备时,就选用一个移动主题。


译:奇葩史