Este artigo foi traduzido por máquina.

CMS de Orchard

Extensibilidade Orchard

Bertrand Le Le

Baixar o exemplo de código

A maioria dos aplicativos Web têm muito em comum, mas ao mesmo tempo apresentam muitas diferenças. Todos eles têm páginas estáticas ("termos de uso," "About Us" e assim por diante). Eles apresentam conteúdo dentro de um layout comum. Eles têm menus de navegação. Eles poderiam ter pesquisa, comentários, classificações e integração de rede social. Mas alguns são blogs, alguns vender livros, alguns manterem amigos em contacto e algumas contêm centenas de milhares de artigos de referência sobre suas tecnologias favoritas.

Um Content Management System (CMS) visa fornecer as peças comuns impondo restrições quanto ao tipo de site está sendo construído. É um exercício delicado de extensibilidade.

Os criadores do CMS pomar (orchardproject.net) — que inclui a mim — optaram por uma abordagem que assenta maciçamente na composição e Convenção. Neste artigo, apresentarei alguns exemplos de extensões simples para o sistema que deve ser um bom ponto de partida para seus próprios módulos.

Um sistema do tipo dinâmico

Não importa quais CMS você usa para construir o seu site, vai haver uma entidade de conteúdo central que vai por nomes diferentes. No Drupal, é chamado um nó e no pomar, é um item de conteúdo. Itens de conteúdo são átomos de conteúdo como blog posts, páginas, produtos, Netvibes ou atualizações de status. Algumas delas correspondem a uma URL, e outros não. Seus esquemas variam muito, mas o que eles têm em comum é que eles são as menores unidades Conteúdas no site. Ou são eles?

Divisão do átomo

Como desenvolvedores, nossa primeira reação é identificar itens de conteúdo como instâncias de classes (post, página, produto ou widget), que é correto para uma medida. Da mesma forma que as classes são compostas de Membros (campos, propriedades e métodos), tipos de conteúdo (as "classes" de itens de conteúdo) são próprios objetos compostos. Em vez de ser composta por propriedades simples com tipos que são elas próprias classes, eles são composto por partes de conteúdo, que são os átomos do comportamento de seu conteúdo. Esta é uma distinção importante, que eu vou ilustrar com um exemplo.

Um post de blog geralmente é composto de uma URL, título, data, corpo de texto rico, uma lista de marcas e uma lista de comentários do usuário. Nenhuma das partes é específica para um post do blog. O que faz um blog post é a composição específica das partes, não apenas qualquer uma das partes.

A maioria dos posts do blog tem comentários, mas comentários também podem ser usados em um site de comércio electrónico para implementar avaliações. Da mesma forma, as marcas são potencialmente útil como uma maneira de classificar qualquer item de conteúdo, não apenas no blog. O corpo de texto rico de um post não é diferente do corpo de uma página. A lista continua. Deve ficar claro neste ponto que a unidade de comportamento em um site da Web é menor do que a unidade de conteúdo.

Outro fato sobre CMSes é que tipos de conteúdo não são fixados antecipadamente. Postagens de blog costumavam ser texto simple, mas rapidamente se tornou muito mais. Eles agora rotineiramente conterem vídeos, podcasts ou imagem galerias. Se você blog sobre suas viagens ao redor do mundo, talvez você vai querer adicionar geolocalização nas suas mensagens.

Outra vez, conteúdos partes vir ao salvamento. Você precisa de uma latitude e longitude? Estender o tipo de post de blog, adicionando uma parte de mapeamento (temos vários disponíveis em nossa Galeria de módulo: Gallery.orchardproject. líquido). Ele rapidamente se torna óbvio quando você pensa sobre isso que esta operação de adição de uma parte para um tipo existente será executada mais frequentemente pelo proprietário do site, não por um desenvolvedor. Portanto, não deve ser possível apenas adicionando uma propriedade complexa para a Microsoft.Tipo de NET Framework. Ele tem de ser orientada por metadados e acontecer em tempo de execução para que possamos construir um admin interface do usuário para fazê-lo (consulte Figura 1).

The Orchard Content Type Editor
Figura 1 O Editor de tipo de conteúdo de pomar

Esta é a primeira maneira de estender o pomar: Você pode criar e estender tipos no vão do admin interface do usuário de conteúdo. Naturalmente, qualquer coisa que você pode fazer do admin interface do usuário, você pode fazer a partir de código, como este:

item.Weld(part);

Este código soldas uma parte para um item de conteúdo dinamicamente. Que é uma possibilidade interessante, pois permite instâncias de tipos para ser estendido dinamicamente em tempo de execução. Em linguagens dinâmicas, isto é chamado um mix-in, mas é um conceito que é quase inédito em linguagens como c#. Isto abre novas possibilidades, mas não é exatamente a mesma coisa que o que estávamos fazendo do admin interface do usuário. Nós também queremos ser capaz de adicionar a parte para o tipo de uma vez por todas, em vez de adicioná-lo para cada instância, conforme mostrado aqui:

ContentDefinitionManager.AlterTypeDefinition(
  "BlogPost", ctb => ctb.WithPart("MapPart")
);

Isso é realmente exatamente como o tipo de conteúdo de post do blog é definido em primeiro lugar:

ContentDefinitionManager.AlterTypeDefinition("BlogPost",
  cfg => cfg
    .WithPart("BlogPostPart")
    .WithPart("CommonPart", p => p
      .WithSetting("CommonTypePartSettings.ShowCreatedUtcEditor", "true"))
      .WithPart("PublishLaterPart")
      .WithPart("RoutePart")
      .WithPart("BodyPart")
  );

Você pode observar este trecho de código que tags e comentários parecem estar em falta de posts do blog. Este é mais um exemplo de separação cuidadoso das preocupações. O módulo de blog realmente sabe nada de tags e comentários, não mais do que as tags e comentários módulos fazem sobre blogs. Um terceiro é responsável por colocá-los juntos.

Receitas gostosas

Durante a instalação, uma receita é executada que é responsável por esse tipo de tarefa. É uma descrição XML da configuração inicial do site. Pomar vem com três receitas de padrão: blog, padrão e core. O código a seguir mostra a parte da receita de blog que adiciona as tags e comentários a posts do blog:

<BlogPost ContentTypeSettings.Draftable="True" TypeIndexing.Included="true">
  <CommentsPart />
  <TagsPart />
  <LocalizationPart />
</BlogPost>

Eu tenho mostrado até agora as várias maneiras em que partes de conteúdo podem ser compostos em itens de conteúdo. Meu próximo passo será a explicar como você pode criar suas próprias peças.

Construindo uma parte

Para ilustrar o processo de construção de uma nova parte, eu vou contar com o exemplo do recurso Meta de meu módulo de indústrias de Vandelay (faça o download do bit.ly/u92283). O recurso Meta adiciona propriedades de descrição para fins de Search Engine Optimization (SEO) e palavras-chave (ver Figura 2).

The SEO Meta Data Editor
Figura 2 O SEO Meta dados Editor

Essas propriedades serão processadas na seção head da página como padrão metatags que os motores de busca entender:

    <meta content="Orchard is an open source Web CMS built on ASP.NET MVC."
      name="description" />
    <meta content="Orchard, CMS, Open source" name="keywords" />

O registro

A primeira peça do quebra-cabeça será uma descrição da forma em que os dados vai ser mantida no banco de dados. Estritamente falando, nem todas as partes precisam um registro, porque nem todas as partes armazenam seus dados no banco de dados de pomar, mas a maioria faz. Um registro é apenas um objeto normal:

public class MetaRecord : ContentPartRecord {
  public virtual string Keywords { get; set; }
  public virtual string Description { get; set; }
}

A classe MetaRecord deriva de ContentPartRecord. Isso não é absolutamente necessário, mas é definitivamente conveniente quanto ele ganha algumas das canalizações fora do caminho. A classe tem duas propriedades de seqüência de caracteres, palavras-chave e descrição. Essas propriedades devem ser marcado como virtuais para que o quadro pode "mix-em" sua própria lógica para construir a classe concreta que será usado em tempo de execução.

Responsabilidade exclusiva do registro é a persistência de banco de dados, com a ajuda de uma declaração para o mecanismo de armazenamento que pode ser encontrada na MetaHandler:

public class MetaHandler : ContentHandler {
  public MetaHandler(
    IRepository<MetaRecord> repository) {
    Filters.Add(
      StorageFilter.For(repository));
  }
}

O armazenamento também tem de ser inicializado. Versões mais antigas do pomar foram inferir o esquema de banco de dados de registros classes, mas supondo que só pode levar você até agora, e isso desde então foi substituído por um sistema de migração mais exato onde modificações de esquema são definidas explicitamente, como mostrado na Figura 3.

Figura 3 explicitamente definido modificações de esquema

public class MetaMigrations : DataMigrationImpl {
  public int Create() {
    SchemaBuilder.CreateTable("MetaRecord",
      table => table
        .ContentPartRecord()
        .Column("Keywords", DbType.String)
        .Column("Description", DbType.String)
    );
    ContentDefinitionManager.AlterPartDefinition(
      "MetaPart", cfg => cfg.Attachable());
    return 1;
  }
}

A tabela MetaRecord é criada com um nome que o sistema será capaz de mapear por Convenção para a classe MetaRecord. Ele tem as colunas do sistema para um registro parte conteúdo adicionado pela chamada para o método ContentPartRecord, plus a palavras-chave e descrição colunas de Cadeia de caracteres que mapeará automaticamente pela Convenção para as propriedades correspondentes da classe de registro.

A segunda parte do método de migração diz que a nova parte será penhorável do admin interface do usuário para qualquer tipo de conteúdo existente.

O método Create sempre representa a migração inicial e normalmente retorna 1, que é o número de migração. A Convenção é que no futuro versões do módulo, o desenvolvedor pode adicionar métodos de UpdateFromX, onde x é substituído pelo número de migração do qual o método Opera. O método deve retornar um novo número de migração que corresponde ao novo número de migração do esquema. Este sistema permite upgrades suaves, independentes e flexíveis de todos os componentes do sistema.

Para representar a parte real, é usada uma classe separada, que eu vou olhar agora.

A classe Part

A representação da parte propriamente dito é outra classe que deriva de ContentPart <TRecord>:

public class MetaPart : ContentPart<MetaRecord> {
  public string Keywords {
    get { return Record.Keywords; }
    set { Record.Keywords = value; }
  }
  public string Description {
    get { return Record.Description; }
    set { Record.Description = value; }
  }
}

A parte atua como um proxy para o registro de palavras-chave e descrição propriedades como uma conveniência, mas se isso não aconteceu, o registro e suas propriedades ainda estaria disponíveis através da propriedade de registro pública da classe base ContentPart.

Qualquer código que tem uma referência para um item de conteúdo que tem a parte MetaPart será capaz de acessar com rigidez de tipos para as palavras-chave e propriedades de descrição, chamando o método como, que é o análogo no sistema de tipo de pomar de um CLR elenco operação:

var metaKeywords = item.As<MetaPart>().Keywords;

A classe de parte é também onde você poderia implementar qualquer comportamento específico de dados da parte. Por exemplo, um produto composto pode expor métodos ou propriedades para acessar seus subprodutos ou calcular um preço total.

Comportamento que pertence à interação do usuário (o código de orquestração que seria em um ASP regular.NET MVC aplicativo ser encontrado no controlador) é outra questão. Isto é onde entram os drivers.

O Driver

Cada parte em um item de conteúdo tem que ter uma oportunidade de participar do ciclo de vida de solicitação e efetivamente fazer a obra de um ASP.NET MVC controlador, mas ele precisa fazê-lo na escala da parte em vez de fazê-lo na escala do pedido completo. Um driver de conteúdo parte desempenha o papel de um controlador de reduzida. Ele não tem toda a riqueza de um controlador em que não há nenhum mapeamento de seus métodos de rotas. Em vez disso, ele é feito de métodos de manipulação de eventos bem definidos, como exibição ou Editor. Um driver é apenas uma classe que deriva de ContentPartDriver.

O método de apresentação é o que obtém chamado quando pomar precisa processar a parte no formulário somente leitura (ver Figura 4).

Figura 4 Método de apresentação do Driver prepara o processamento da parte

protected override DriverResult Display(
  MetaPart part, string displayType, dynamic shapeHelper) {
  var resourceManager = _wca.GetContext().Resolve<IResourceManager>();
  if (!String.IsNullOrWhiteSpace(part.Description)) {
    resourceManager.SetMeta(new MetaEntry {
      Name = "description",
      Content = part.Description
    });
  }
  if (!String.IsNullOrWhiteSpace(part.Keywords)) {
    resourceManager.SetMeta(new MetaEntry {
      Name = "keywords",
      Content = part.Keywords
    });
  }
  return null;
}

Este driver é na verdade um pouco atípico, porque a maioria dos drivers resultar em processamento (mais sobre isso daqui a pouco), Considerando que a parte de Meta precisa processar sua metatags na cabeça simple no lugar. A seção de cabeçalho de um documento HTML é um recurso compartilhado, portanto, são necessárias precauções especiais. Pomar fornece APIs para acessar esses recursos compartilhados, que é o que você está vendo aqui: Eu estou passando o Gerenciador de recursos para definir os metatags. O Gerenciador de recursos vai cuidar do processamento das marcas reais.

O método retorna null porque não há nada para processar no local neste cenário específico. A maioria dos métodos de driver em vez disso irão retornar um objeto dinâmico chamado uma forma, que é análoga a um modelo de exibição em ASP.NET MVC. Eu vou voltar a formas em um momento quando chega a hora de torná-los em HTML, mas para o momento, basta dizer que eles são objetos muito flexíveis onde você pode ficar tudo o que vai ser relevante para o modelo que irá processá-lo, sem ter que criar uma classe especial, conforme mostrado aqui:

protected override DriverResult Editor(MetaPart part, dynamic shapeHelper) {
  return ContentShape("Parts_Meta_Edit",
    () => shapeHelper.EditorTemplate(
      TemplateName: "Parts/Meta",
      Model: part,
      Prefix: Prefix));
}

O método de Editor é responsável por preparar o processamento do editor de interface do usuário para a parte. Ele normalmente retorna um tipo especial de forma que seja apropriado para a construção de interfaces do usuário edição composto.

A última coisa que o driver é o método que manipulará postos do editor:

protected override DriverResult Editor(MetaPart part,
  IUpdateModel updater, dynamic shapeHelper) {
  updater.TryUpdateModel(part, Prefix, null, null);
  return Editor(part, shapeHelper);
}

O código nesse método chama TryUpdateModel para atualizar automaticamente a parte com dados que foi postados de volta. Uma vez que é feito com esta tarefa, ele liga para o primeiro Editor Método para retornar a mesma forma de editor que ele estava produzindo.

Formas de processamento

Chamando para todos os drivers para todas as partes, pomar é capaz de construir uma árvore de formas — um modelo grande exibição composto e dinâmico para a solicitação de todo. Sua próxima tarefa é descobrir como cada uma das formas de resolver em modelos que serão capazes de processá-los. Ele faz pela olhando para o nome de cada forma (Parts_Meta_Edit no caso do método Editor) e tentar mapear que para arquivos em lugares bem definidas do sistema, tais como o tema atual e o módulo de exibições de pastas. Este é um ponto de extensibilidade importante porque permite que você substituir o processamento padrão de qualquer coisa no sistema, deixando apenas um arquivo com o nome certo em seu tema local.

Na pasta Views\EditorTemplates\Parts do meu módulo, eu deixei cair um arquivo chamado Meta.cshtml (consulte Figura 5).

Figura 5 modelo de Editor para o MetaPart

    @using Vandelay.Industries.Models
    @model MetaPart
    <fieldset>
      <legend>SEO Meta Data</legend>
      <div class="editor-label">
        @Html.LabelFor(model => model.Keywords)
      </div>
      <div class="editor-field">
        @Html.TextBoxFor(model => model.Keywords, new { @class = "large text" })
        @Html.ValidationMessageFor(model => model.Keywords)
      </div>
      <div class="editor-label">
        @Html.LabelFor(model => model.Description)
      </div>
      <div class="editor-field">
        @Html.TextAreaFor(model => model.Description)
        @Html.ValidationMessageFor(model => model.Description)
      </div>
    </fieldset>

Tudo é conteúdo

Antes de passar para outros tópicos de extensibilidade, eu gostaria de mencionar que depois que você compreende o sistema de tipos de item de conteúdo, você sob­repousar o conceito mais importante no pomar. Muitas entidades importantes do sistema são definidas como itens de conteúdo. Por exemplo, um usuário é um item de conteúdo, que permite que os módulos de perfil adicionar propriedades arbitrárias para eles. Temos também itens de conteúdo widget, que pode obter processado em zonas que define um tema. Isso é como os formulários de pesquisa, arquivos do blog, nuvens do tag e outro sidebar interface do usuário são criadas no pomar. Mas o mais surpreendente o uso de itens de conteúdo pode ser o próprio site. Configurações do site são itens efetivamente conteúdos no pomar, que faz muito sentido, uma vez que você entende como multitenancy é gerenciado no pomar. Se você quiser adicionar suas próprias configurações de site, tudo que você tem a fazer é adicionar uma parte para o tipo de conteúdo de Site, e você pode construir uma admin edição interface do usuário para ela seguindo os mesmos passos exatos que descrevi anteriormente. Um sistema de tipo de conteúdo extensível unificada é um conceito extremamente rico.

Extensões de embalagem

Mostrei como construir seu próprio peças para pomar, e já fez alusão aos conceitos de módulos e temas sem definir esses termos. Em suma, eles são as unidades de implantação no sistema. Extensões são distribuídas como módulos, e a aparência visual é distribuída como temas.

Um tema é normalmente um monte de imagens, folhas de estilo e modelo de substituições, empacotadas em um diretório sob o diretório de temas. Ele também tem um theme.txt arquivo na sua raiz para definir metadados, como o autor do tema de manifesto.

Da mesma forma, um módulo é um diretório sob o diretório de módulos. É também um ASP.Área de NET MVC, com algumas reviravoltas. Por exemplo, ele precisa de um arquivo de manifesto module.txt adicionais que declara alguns metadados para o módulo, como seu autor, site da Web, nomes de recursos, dependências ou número de versão.

Sendo apenas uma área de um site maior, um módulo precisa jogar bonito com alguns recursos compartilhados. Por exemplo, as rotas que ele usa devem ser definidas por uma classe que implementa IRouteProvider. Pomar vai construir a tabela de rota completa de que foi contribuído por todos os módulos. Da mesma forma, módulos podem contribuir para a construção do menu admin implementando a interface INavigationProvider.

Curiosamente, o código para os módulos geralmente não é entregue como binários compilados (embora isso seja tecnicamente possível). Esta foi uma decisão consciente de que fizemos para incentivar o módulo de hacking, onde você começa de um módulo que você faça o download da Galeria e ajustá-lo para atender às suas necessidades específicas. A possibilidade de ajustar o código para qualquer coisa é um dos pontos fortes de um CMS PHP como Drupal ou WordPress, e queríamos fornecer esse mesmo tipo de flexibilidade no pomar. Quando você faz o download de um módulo, você baixar o código-fonte, e que o código obtém compilado dinamicamente. Se você fizer uma alteração em um arquivo de origem, a alteração Obtém pegou e o módulo é recompilado.

Injeção de dependência

Até agora, eu tenho focada em um tipo de extensibilidade, o sistema de tipos, porque que representa a maioria dos módulos do pomar, mas há muitos outros pontos de extensibilidade — longe demais, na verdade, para mim listar aqui. Eu ainda quero adicionar algumas coisas sobre os princípios gerais que estão a trabalhar em todo o quadro.

Uma coisa importante para obter direito em um sistema altamente modular é menos rigidez. No pomar, quase tudo acima sanitários de baixo nível é um módulo. Mesmo módulos são gerenciados por um módulo! Se você deseja que esses módulos para trabalhar como independentemente um do outro quanto possível — se você quer uma implementação de um recurso para ser swap com outro — você não pode ter dependências de disco rígidas.

Uma maneira fundamental para alcançar este objetivo é usar injeção de dependência. Quando você precisa usar os serviços de outra classe, apenas não instanciá-lo, como que estabeleceria uma dependência difícil dessa classe. Em vez disso, você injetar uma interface que implementa esta classe, como um parâmetro de Construtor (veja Figura 6).

Figura 6 injetando dependências por meio de parâmetros de Construtor

private readonly IRepository<ContentTagRecord> _contentTagRepository;
private readonly IContentManager _contentManager;
private readonly ICacheManager _cacheManager;
private readonly ISignals _signals;
public TagCloudService(
  IRepository<ContentTagRecord> contentTagRepository,
  IContentManager contentManager,
  ICacheManager cacheManager,
  ISignals signals)
  _contentTagRepository = contentTagRepository;
  _contentManager = contentManager;
  _cacheManager = cacheManager;
  _signals = signals;
}

Desta forma que sua dependência é sobre a interface, não a classe e a implementação pode ser trocada sem ter que alterar seu código. A implementação específica da interface que obtém injetado já não é a decisão do consumidor da interface. Controle é inverso aqui, e é o quadro que faz com que essa decisão.

Naturalmente, isto não se limita a interfaces que define pomar. Qualquer módulo pode fornecer seus próprios pontos de extensibilidade, declarando apenas uma interface que deriva de IDependency. É realmente tão simples.

Alguns outros pontos de extensibilidade

Eu tenho apenas uma pequena amostra de extensibilidade aqui. Existem muitas interfaces que podem ser usados no pomar para estender o sistema de forma criativa. Pode-se dizer mesmo que pomar é essencialmente nada, mas um mecanismo de extensibilidade. Todas as peças do sistema são swap e extensível.

Antes de eu terminar este artigo, eu vou mencionar algumas das interfaces mais úteis no sistema que você pode fazer check-out. Eu não tenho espaço quase suficiente aqui para entrar em profundidade, mas pode dar-lhe ponteiros, e você pode ir para o código e siga o uso das interfaces. Esta é uma maneira realmente grande para aprender, pelo caminho.

  • IWorkContextAccessor permite que o seu código acessar o contexto de trabalho para a solicitação atual. O contexto de trabalho, por sua vez, fornece acesso para o HttpContext, atual Layout, configuração de sites, usuário, tema e cultura. Ele também fornece recursos para obter uma implementação de uma interface, de lugares onde você não pode fazer a injeção de dependência ou onde você precisará Atrasá-lo até que após a construção.
  • IContentManager fornece tudo o que você precisa para consultar e gerenciar itens de conteúdo.
  • IRepository <T> dá acesso a métodos de acesso de dados de nível inferior, para aqueles momentos quando IContentManager não é suficiente.
  • IShapeTableProvider permite que uma batelada de cenários de manipulação de forma on-the-fly. Basicamente, você ligar para eventos sobre formas, e com eles você pode criar formas alternativas para ser usado em certas situações, formas de transformar, adicionar membros a elas, movê-las no layout e assim por diante.
  • IBackgroundTask, IScheduledTask e IScheduled­TaskHandler são as interfaces para usar se você precisar de tarefas atrasadas ou repetidas para ser executado em segundo plano.
  • IPermissionsProvider permite que seus módulos para expor suas próprias permissões.

Saiba Mais

Pomar é uma potência de extensibilidade e apresentar tudo que ela tem para oferecer neste espaço limitado é um desafio. Minha esperança é que eu dei-lhe a vontade de aprender mais sobre ele e mergulhar fundo nele. Temos uma Comunidade simpática e animada orchard.codeplex.com/discussions que terá prazer em orientá-lo e responder às suas perguntas. Com tanta coisa para implementar e tantas idéias para explorar, aqui é uma excelente oportunidade para fornecer uma contribuição significativa.

Bertrand Le Roy iniciou sua carreira de desenvolvedor profissional em 1982 quando ele publicou seu primeiro vídeo game. Ele lançou em 2002 o que foi provavelmente o primeiro CMS para executar em ASP.NET. Um ano mais tarde, ele foi contratado pela Microsoft ASP.NET time e mudou-se para os Estados Unidos. Ele tem trabalhado em ASP.NET versões 2.0 para 4 e ASP.NET AJAX; contribuíram para que a jQuery parte oficial da.NET caixa de ferramentas do desenvolvedor; e representa Microsoft no Comité de direcção OpenAjax Alliance.

Graças ao seguinte especialista técnico para revisão deste artigo: Sebastien Ros