SCSF - Part 5 Dependency Injection and the Composite Application Block

Introduction

In part 1 of this series of articles I described a simple CAB application. This had three Windows Application C# projects with no references to each other. In spite of this, with some very simple code we could get all three to launch their individual screens. That very simple application didn’t have the projects interacting in any other way, however.

 

part1 描述了 最简单的cab应用程序, 它拥有三个彼此没有相互引用的 windows 应用程序,尽管如此, 用一些很简单的代码我们也可以让这仨调用他们自己的form, 但是 他们直接并不能相互通信。

 

Part 2 of the series described WorkItems, which can be thought of as containers for code, and how we could add a WorkItem to each of our projects in a hierarchy.

part2 描述了workitems, 我们可以把他看做是一个code的容器,并且我们是如何将workitem 加到我们的projects里的

 

Part 3introduced dependency injection as a way of structuring our code so that our class structure was loosely coupled and behaviour could be easily changed by changing which class was ‘injected’ into another.

part3 介绍了依赖注入是一种结构化我们代码的方法 我们的代码结构可以变得很松散,并且可以轻易地通过更改注入类来更改功能。

 

In this article I will bring all of these ideas together and explain how dependency injection works in the CAB.

这篇文章 我将把这些内容串在一起,并且解释依赖注入是怎么在cab里运作的。

 

The Problem

We want to get our three projects from part 1 (Red, Blue and Shell) to interact with each other without having them reference each other. As discussed in part 2, WorkItems are designed to allow us to do this: we can put a WorkItem in each project, put code into their various collections, share the WorkItems and thus share the code.

But how does one project know about the WorkItem from another project? Bear in mind that there are no direct references between the projects. This could be done manually in code using reflection, of course. But the CAB framework gives us a much cleaner way to do this.

我们想让我们的三个项目可以相互通信却不相互引用, 正如part2 里提到的,workitems就是为了这个目的而被设计出来的,我们可以让每个project里都放置一个workitem, 把代码房子他们的collections里面,共享workitem 从而共享了code。

 

 

Dependency Injection and the CAB

The answer is we can use dependency injection to inject a WorkItem from one project or ‘module’ into another.

This is clearly an appropriate thing to do here: we want loose coupling between our modules and flexibility to change how they interact. As I’ve discussed in another article, in extreme cases we might have different development teams responsible for different modules, with different release cycles. Using dependency injection one team could change a class that’s injected and thus change the behaviour of another module without that module needing to be re-released.

However, unlike in my example in part 3, dependency injection in the CAB doesn’t use configuration files to specify which class should be used. Instead attributes are used to tell the code that a dependency needs to be injected.

答案是我们可以使用依赖注入 去注入一个workitem 从一个 project或者 module到另一个。 这是我们可以做的非常合适的事让我使得模块之间更松耦合,更具伸展性。 就像我以前讨论过的, 有个case, 我们也许有连个不同的开发小组负责不同的有着不一样发布周期的模块,使用依赖注入一个小组可以通过改变注入的class 使得另一个模块的表现发生变化 并且不需要另外一个模块重新发布。

然而,不像我在part3里解释的, cab里的依赖注入并不适用配置文件去具体化哪个class需要被使用, 我们在这里使用 attribute

 

Example

This is most easily seen with an example. We have already seen that a root WorkItem is created in our CAB application at start up. We have also seen that all modules listed in the ProfileCatalog.xml file will get loaded at the start up of a CAB application, and that a Load() method in a ModuleInit class gets called in each module.

We want a reference to the root WorkItem in a module that is not the shell. We can achieve this by putting a setter for a WorkItem in our ModuleInit class for the module, along with an attribute:

这是一个最简单的例子, 我们已经看到 这个root workitem 在cab 程序启动的时候被创建了, 我们还看到其他所有在 profilecatalog.xml 文件里的模块会在cab应用程序开始的时候被加载 ,而且, 在moduleinit 类的load()方法会被调用。

我们想在不是shell的module里有 对workitem的引用, 我们可以通过为workitem设置一个有属性的setter来达到这点。

private WorkItem parentWorkItem;
[ServiceDependency]
public WorkItem ParentWorkItem
{
    set
    {
        parentWorkItem = value;
    }
}
As you can see we decorate the setter with the attribute ‘ServiceDependency’. This tells the CAB framework that when it is loading this module it should look for an appropriate WorkItem to ‘inject’ into this setter.

If we put this code into the RedModuleInit class in our example, and put a breakpoint in the setter we can see that the root WorkItem is being passed into here at start up and stored in the parentWorkItem variable.

像你所看到的,我们给set 装饰上了attribute“ServiceDependency',这告诉了cab的framework 当它在装载模块的时候,它需要寻找一个合适的workitem去注入。

如果我们把这代码放到这个例子中的redimoduleinit 这个class里,并且设置一个断点在setter上,我们可以发现在启动的时候 rootworkitem 被pass到这里,并且被存在 parentworkitem 这个变量里。

 

How is this Working (1)?

You may wonder how the CAB knows what to inject and where to inject it here. After all there may be multiple WorkItems in our project: which one should it choose? Furthermore we can inject different types (i.e. not WorkItems) in a similar way. If we have several instantiated classes of the same type how do we inject a specific one? And how does the CAB find the ServiceDependency attribute? Does it scan all classes in all modules?

I’m going to leave these issues for now: just accept that the root WorkItem gets injected in this case. I’ll return to this later in this article.

你也许会感到疑惑 cab是怎么知道inject什么,inject到哪里,毕竟有也许在我们的project里有许多workitem,哪一个会被选中,此外我们可以通过类似的办法注入其他不同的类型(不仅是workitems), 如果我们有许多 实例化同一个类的对象, 我们如何去注入一个指定的? 而且cab又是如何找到servicedependency 这个属性的呢? 它扫面了所有的module了吗? 我将先搁置这些问题, 只是接受 root workitem 被注入了。 我会在文章的后面再次提及。

 

Red and Blue Forms Application

So we can get a reference to the root WorkItem as above. In our na?ve CAB application from part 1 we’d quite like to tell the red and blue forms in the modules to load as MDI children into the shell form.

We can do this by firstly adding the shell form to the Items collection of the root WorkItem. Then if the root WorkItem is available in our Red and Blue projects we can access the shell form through the Items collection.

There’s an AfterShellCreated event of the FormShellApplication class that we can override in our program class to add the shell form to the Items collection:

我们得到了 rootworkitem的 引用, 在part1,在我们的cab 应用程序里,我们十分想让红与蓝项目能够成为shell form的子项目,我们可以首先可以把shellform 加到rootworkitem里, 然后如果rootworkitem在红蓝项目里能够被用到, 那我们就可以通过 item collection 访问到 shell form了

public class Program : FormShellApplication<WorkItem, Form1>
{
    [STAThread]
    static void Main()
    {
        new Program().Run();
    }
    protected override void AfterShellCreated()
    {
        base.AfterShellCreated();
        this.Shell.IsMdiContainer = true;
        RootWorkItem.Items.Add(this.Shell, "Shell");
    }
}


Note that the shell gets a name in the Items collection (“Shell”). Note also that we’re making the shell form into an MDIContainer here, accessing it via the Shell property of the FormShellApplication class.

In the Load method of our modules we can now retrieve the shell form and set it to be the MDIParent of our red and blue forms. So our ModuleInit class looks as below:

注意到 这个item有个名字 "shell“, 注意到我们也将这个shell form 放到 mdi container 里,可以通过这个formshellapplication类里的shell属性访问到。 在我们的load方法里, 我们现在可以得到 shell form 并将它设置成 红和蓝 forms 的midiparent。

public class RedModuleInit : ModuleInit
{
    private WorkItem parentWorkItem;
    [ServiceDependency]
    public WorkItem ParentWorkItem
    {
        set
        {
            parentWorkItem = value;
        }
    }             
    public override void Load()
    {
        base.Load();
        Form shell = (Form)parentWorkItem.Items["Shell"];
        Form1 form = new Form1();
        form.MdiParent = shell;
        form.Show();
    }
}

If we now run the application our red and blue forms will appear as MDI children of the main shell.

The code for this is available. By the way you should know that there are better ways of setting up an MDI application in the CAB: this example is intended to just show the basic concepts of dependency injection.

如果我们现在在跑这个应用程序 我们的红蓝 form将会作为子  出现在 main shell里,顺便说一句, 你应该知道 有许多更好的方法来实现这个, 这个例子主要是告诉你一些基本的依赖注入的概念。

 

How is this Working (2)?

Earlier in this article I posed several questions about how all this could be working. I’ll attempt to answer those questions now.

As discussed earlier, WorkItems are generic containers for code to be passed between modules, and are capable of being arranged in a hierarchy. But in addition they are actually ‘Inversion of Control containers’ or ‘Dependency Injection containers’. I mentioned these in part 4 of this series of articles. However, I’ve rather glossed over them up until now. Note that both Spring and PicoContainer use containers to control their dependency injection.

在这篇文章的前面,我提了一些问题,关于这个是如何实现的,现在我将试着去回答这些问题。 正如我们之前讨论过的, workitems 是一个泛型的containers,可以让代码传入到workitem里, 并且能够 被安排成 树状结构, 并且他们是 反转控制containers 和 依赖注入containers,然而,知道现在我一直都在粉饰, spring和picocontainer 使用containers 来控制依赖注入。

 

WorkItems as Dependency Injection Containers

These containers work in the CAB as follows. Suppose we want to inject object A into object B. The dependency injection only happens when object B is added into an appropriate collection on a WorkItem. This can be on creation of the object if we create object B with the AddNew method, or it can happen with an existing object if we use the Add method to add it to a WorkItem collection.

Furthermore normally the injection can only work if object A is already in an appropriate collection of the same WorkItem. The exception is if we are using the ‘CreateNew’ attribute (see below). In this case object A will be created and added to the Items collection of the WorkItem before being injected.

As you can see, in a way dependency injection in the CAB is ‘scoped’ to a WorkItem.

这些containers 在cab里是这样工作的, 假设我们想将 object A 注入到 object B,这个依赖注入 只有在 当我们将 object B 作为一个workitem加入到一个合适的 collection里的时候才会发生。 我们可以通过 addnew 方法来实现这一点,或者我们可以先new 然后再通过add方法加入。

此外, 通常情况下 这个注入只有在object A 本来就在同一个workitem的一个合适的collection里, 除非我们用 createnew 属性标记了 object A,在这种情况下 object A 在被注入之前会被创建并且加入到这个item collection里

 

Types of Dependency Injection in the CAB

There are three attributes that can be attached to setters and used for dependency injection in the CAB:

有三种attribute可以加在setters之上用于依赖注入。

  1. ComponentDependency(string Id)
    This attribute can be used to inject any object that already exists in a WorkItem’s Items collection. However, because we can have multiple objects of the same type in this collection we have to know the ID of the item we want to inject (which is a string). We can specify an ID when we add our object into the collection. If we don’t specify an ID the CAB assigns a random GUID to the item as an ID. Note that if the object does not exist in the appropriate Items collection when we try to inject it then the CAB will throw a DependencyMissingException.
  2. ServiceDependency
    We’ve seen this attribute already. An object must be in the WorkItem’s Services collection to be injected using this attribute. The Services collection can only contain one object of any given type, which means that the type of the setter specifies the object uniquely without the need for an ID. I will discuss Services further in part 6 of this series of articles.
  3. CreateNew
    A new object of the appropriate type will be created and injected if this attribute is attached to a setter. The new object will be added to the WorkItem’s Items collection.

 

    1.  ComponentDependency(string ID)

         这一attribute可以被用于注入任何已经在workitem的items collection里的对象,然而,因为我们能拥有同一种类型的多种对象,我们必须知道我们需要注入的对象的id ,它是一个字符串,我们可以给我们加入到collection里的object加一个id,如果我们不明确加一个id,cab会随机分配一个guid作为id,注意如果这个对象不存在于合适的item collection里,当我们试着给他加注入的时候,CAB就会报一个DependencyMissingException的错误出来。

    2.ServiceDependency

      我们已经看到了这个attribute了,使用这个attribute的时候,该对象必须在workitem的service collection里才能被注入,service collection 只能包含给定类型的一个对象,意味着这不需要id, 我将在part6继续讨论 services

    3.CreateNew

      如果这个attribute被加在了setter上的时候,一个适合类型的新对象会被创建并且injected,新对象会被加入的workitem的item collection里。

 

As usual this is best seen with an example.

Example

We set up a CAB project with two component classes. Component1 is just an empty class, whilst Component2 has two private Component1 member variables that will be injected. One will be injected by name (and so needs to be created and added to the WorkItem’s Items collection prior to injection). One will be injected by being created:

我们新建了一个cab的项目,该项目里有两个component的类, component1 是一个空的class, component2 有两个会被注入的私有的component1的变量,一个会被通过已有的名字注入,另一个则是通过createnew注入。

public class Component2
{
    private Component1 component11;
    [ComponentDependency("FirstComponent1")]
    public Component1 Component11
    {
        set
        {
            component11 = value;
        }
    }             
    private Component1 component12;
    [CreateNew]          
    public Component1 Component12
    {
        set
        {
            component12 = value;
        }
    }
}


To use this we put the following code in the AfterShellCreated method of our FormShellApplication class:

protected override void AfterShellCreated()
{
    RootWorkItem.Items.AddNew<Component1>("FirstComponent1");
    Component2 component2 = new Component2();
    RootWorkItem.Items.Add(component2);
    DisplayRootItemsCollection();
}
Notice the syntax of the AddNew command for the Items collection. It’s a generic method. Remember that a generic is simply a way of providing a type (in this case a class) at runtime. Here we are providing the type “Component1” to the AddNew generic method. A generic method can do anything it likes with the type provided. Here AddNew will instantiate that type and add it to the items collection.

注意到 addnew 命令的语法, 这是一个泛型的方法, 记住泛型是一种简单的方法在运行时提供类型,现在我们提供了component1,将其加入到addnew 的泛型方法里, 一个泛型方法可以做任何给定类型的事,在这里,addnew 会实例化这个类型,并且将它加到我们的 items collection里。

 As you can see, we create a Component1 object with ID “FirstComponent1” and add it to the Items collection. We then create a Component2 object using the ‘new’ keyword. We would usually do this using AddNew, but I want to demonstrate that we don’t have to do this. Next we add the Component2 object to the Items collection.

 正如你所看到的,我们通过id ”firstcomponent1“创建了一个component1 的对象,并且将它加入到我们的items collection里,然后我们通过new创建了 component2 对象 ,我们应该使用addnew方法,但是我只是想说明我们未必一定要这么做,然后我们将component2加入到 这个items 的 collection里。

 At this point the “FirstComponent1” object will be injected into component2 in the setter marked with the “ComponentDependency” attribute. Also another Component1 object will be created and injected into component2 in the setter marked with the “CreateNew” attribute.

在这里 “firstcomponent1“ 对象会被 通过 拥有“ComponentDependency” attribute的setter注入到 component2中,同时 component1 对象会被创建并且通过 拥有“CreateNew” 的attribute 注入到 component2 中。

 Finally in this code we call a routine called DisplayRootItemsCollection:

private void DisplayRootItemsCollection()
{
    System.Diagnostics.Debug.WriteLine("ITEMS:");
    Microsoft.Practices.CompositeUI.Collections.ManagedObjectCollection<object> coll = RootWorkItem.Items;
    foreach (System.Collections.Generic.KeyValuePair<string, object> o in coll)
    {
        System.Diagnostics.Debug.WriteLine(o.ToString());
    }
}
This just dumps out all the objects in the Items collection to the debug window. The results are as below:

ITEMS:
[4e0f206b-b27e-4017-a1b2-862f952686da, Microsoft.Practices.CompositeUI.State]
[14a0b6a2-12a4-4904-8148-c65802af763d, Shell.Form1, Text: Form1]
[FirstComponent1, Shell.Component1]
[4c7e0a20-90b7-42c6-8912-44ecba40523f, Shell.Component2]
[c40a4626-47e7-4324-876a-6bf0bf99c754, Shell.Component1]

As you can see we’ve got two Component1 items as expected, one with ID “FirstComponent1” and one with ID a GUID. And we have one Component2 item as expected. We can also see that the shell form is added to the Items collection, as well as a State object.

The code for this is available, and if you single-step through it you can see the two Component1 objects being injected into component2.

正如你所看到的, 我们得到了我们期望得到的两个component1, 一个的id是“FirstComponent1”,另一个是guid,我们还拥有我们所期待的 component2, 我们还能看到 shell form 和 state object,如果你单步调试它,你会看到 这两个component1被注入到了 component2里。

 

Where Was All This in the Original Example?

Note that in the original example in this article the root WorkItem was injected into a ModuleInit class apparently without the ModuleInit class being added to any WorkItem. This seems to contradict the paragraphs above that say that we can only inject into objects that are put into WorkItems. However, the CAB framework automatically adds ModuleInit classes into the root WorkItem when it creates a module, so we don’t need to explicitly add them ourselves for the dependency injection to work.

注意到在这篇文章的第一个例子里,在 ModuleInit class 没有被加入到 任何workitem的前提下 rootworkitmen 被注入到 一个  ModuleInit class 里,这似乎违背了上面那段话所说的, 我们只能注入被加入到workitems里的objects, 然而,当module被创建的时候,cab framework 自动的把 ModuleInit classes 加入到root workitem,所以我们没必要显式的再加入他。

 

Futhermore, the root WorkItem that was injected as a ServiceDependency even though it had not been explicitly added to any Services collection. Again this seems to contradict the statements above that any object being injected must be in an appropriate collection. But the code works because any WorkItem is automatically a member of its own Services collection.

此外,这个rootworkitem会被作为servicedependency注入,即使它没有被显性的加入到 service collection里,再次说明,这似乎违法了上面所声明的 任何被注入的对象必须被加入到一个合适的collection里,但是这代码奏效是因为 这workitem 会自动的成为 service collection的一员。

 

 

You can see this if you download and run this example. It is an extension of the original example that allows you to output both the Items collection and the Services collection to the output window via a menu option. If you do this after the application has loaded you get the output below:

连接里的例子是原来那个例子的扩展,它允许你同属输出 item collection 和 services collection 到输出窗体的 菜单选项里, 如果你在程序启动后做这些,你将得到如下:

 

ITEMS:
[336ad842-e365-47dd-8a52-215b951ff2d1, Microsoft.Practices.CompositeUI.State]
[185a6eb5-3685-4fa7-a6ee-fc350c7e75c4, Shell.Form1, Text: Form1]
[10d63e89-4af8-4b0d-919f-565a8a952aa9, Shell.MyComponent]
[Shell, Shell.Form1, Text: Form1]
[21ac50d7-3f22-4560-a433-610da21c23ab, Blue.BlueModuleInit]
[e66dee6e-48fb-47f0-b48e-b0eebbf4e31b, Red.RedModuleInit]
SERVICES:
[Microsoft.Practices.CompositeUI.WorkItem, Microsoft.Practices.CompositeUI.WorkItem]
…(Complete list truncated to save space)

You can see that both the BlueModuleInit and RedModuleInit objects are in the Items collection in spite of not being explicitly added by user code, and the WorkItem is in the Services collection.

你可以看到  BlueModuleInit  和 RedModuleInit  都在item collection里 尽管它们没有被显式地加入到代码中, 且workitem也在Services collection 里

 

ObjectBuilder

To understand and use the Composite Application Block you don’t need to understand in detail its underlying code. It’s intended to be used as a framework after all. However, it’s useful to know that the dependency injection here is all done by the ObjectBuilder component.

为了理解并使用Composite Application Block  你没必要 理解最底层代码的细节, 毕竟那是framework, 然而,知道依赖注入是由ObjectBuilder 完成的 这很有用。

 

When we call AddNew or Add on a collection of a WorkItem it’s the ObjectBuilder that looks at the dependency attributes on the class we’re adding and injects the appropriate objects.

当我们调用AddNew  或者 Add  方法的时候,那是ObjectBuilder在寻找 依赖注入的属性,在这个类里 我们加入并且注入 适当的对象。

 

The ObjectBuilder is a ‘builder’ in the classic design patterns sense. The builder pattern ‘separates the construction of a complex object from its representation so that the same construction process can create different representations’.

ObjectBuilder  是 builder pattern 设计模式里的builder 类,这一模式将构造和复杂对象分离开,如此同一构造顺序可以创建出不同的表现。

 

Note that this pattern is often called a ‘factory pattern’, although factories in the Gang of Four ‘Design Patterns’ book are slightly different things (we’re not creating families of objects (Abstract Factory) or ‘letting ‘the subclasses decide which class to instantiate’ (Factory Method)).

注意这一模式经常被叫做  ‘factory pattern’ 虽然 在Gang of Four 的设计模式这本书里 factories是另外一回事。

 

WorkItems in a Hierarchy and Dependency Injection of Items

As discussed previously, one of the strengths of WorkItems is that multiple instances can be instantiated in different modules, and they can all be arranged in a hierarchy. This is because each WorkItem has a WorkItems collection. However, you should be aware that dependency injection only works for items in the current WorkItem. If you attempt to inject an object in a different WorkItem in the hierarchy into an object in your WorkItem you will get a DependencyMissingException.

像我们之前讨论过的, WorkItems 的一大优势在于 复数的实例可以在不同的modules里被实例化, 而且他们能被安排成树状结构,这是因为每一个workitem 都有一个workitems collection,然而, 你需要知道 依赖注入 只能在当前的 workitem中奏效, 如果你尝试着 注入一个object到不同的workitem里,你会得到如下错误:DependencyMissingException

 

We can see this by modifying the AfterShellCreated event of our FormShellApplication in the example using Component1 and Component2 above:

我们可以通过修改上面的代码来看:

WorkItem testWorkItem = null;
protected override void AfterShellCreated()
{
    testWorkItem = RootWorkItem.WorkItems.AddNew<WorkItem>();
    RootWorkItem.Items.AddNew<Component1>("FirstComponent1");
    // The next line throws an exception as the testWorkItem
    // container doesn't know about FirstComponent1, and Component2
    // is asking for it to be injected.
    testWorkItem.Items.AddNew<Component2>();
    DisplayRootItemsCollection();
}
Here we add a new WorkItem to our RootWorkItem. We add an instance of Component1 with ID “FirstComponent1” to our RootWorkItem as before. Then we add an instance of Component2 to our testWorkItem.

现在我们加入一个WorkItem 到RootWorkItem,我们加入 Component1 with ID “FirstComponent1” 的实例到我们的RootWorkItem 里,然后我们加入Component2 的实例到testWorkItem。

 

Remember that Component2 asks for a Component1 object with ID “FirstComponent1” to be injected when it is created. Because the testWorkItem knows nothing about such an object we get an exception thrown.

记住 当Component2被创建的时候,它寻找 id为 ”FirstComponent1“ 的 Component1 ,因为testWorkItem 对”FirstComponent1“ 的 Component1  一无所知, 所以就报了这个错。

 

We can fix the code by adding our Component1 into the testWorkItem instead of the RootWorkItem:

我们可以这样修复它:

testWorkItem.Items.AddNew<Component1>("FirstComponent1");
 

The code for this example is available.

WorkItems in a Hierarchy and Dependency Injection of Services

Services behave differently to the example given above.

Services 的表现则另有不同

We can make Component1 a service by adding it to the Services collection of the RootWorkItem instead of the Items collection, and telling Component2 it’s a ServiceDependency and not a ComponentDependency. Then the code will work. This is because the CAB includes a service locator that looks in all parent WorkItems of the current WorkItem to see if a given service is available. I will discuss this in more detail in part 6.

我们可以通过将Component1加入到  RootWorkItem  的Services collection 而不是Items collection 让其变成service ,并且告诉Component2  那是一个ServiceDependency 而不是ComponentDependency,然后这代码就能奏效了,这是因为CAB 会去找当前WorkItem 的所有parent WorkItems  去看看有没有 sevice 可用,我们将在第part6里更多的讨论

 

Conclusion

Dependency injection in the CAB is a powerful tool. It enables us to share code between modules in a loosely-coupled way.

In part 6 of this series of articles I discuss how we can use the CAB to do constructor injection. Part 7 of the series will investigate the Services collection of a WorkItem in some detail.

CAB 的 Dependency injection是一个非常强大的工具, 它能使我们在不同modules  share代码。在part6,我将会讨论我们如何使用cab的结构注入,Part 7会讨论Services collection

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值