所以,这就是我想出解决方案的方式.....
第一个胜利是我发现了 2 篇优秀的教程博客文章,这个解决方案站在它们的肩膀上,非常尊重以下代码猫:
Tim Geyssons - Nibble 帖子:
http://www.nibble.be/?p=440
马库斯·约翰逊 - Enkelmedia
http://www.enkelmedia.se/blogg/2013/11/22/creating-custom-sections-in-umbraco-7-part-1.aspx
-
创建一个模型对象来表示一个关键字短语,该关键字短语将与一个新的简单 ORM 表相关联。
ToString() 方法允许在前端输出一个友好的名称。
[TableName("Keyphrase")]
public class Keyphrase
{
[PrimaryKeyColumn(AutoIncrement = true)]
public int Id { get; set; }
public string Name { get; set; }
public string Phrase { get; set; }
public string Link { get; set; }
public override string ToString()
{
return Name;
}
}
-
创建一个 Umbraco“应用程序”,它将通过实现 IApplication 接口来注册新的自定义部分。我已将我的“实用程序”称为“实用程序”并将其与实用程序图标相关联。
[Application("Utilities", "Utilities", "icon-utilities", 8)]
public class UtilitiesApplication : IApplication { }
装饰器允许我们提供新自定义部分的名称、别名、图标和排序顺序。
-
创建一个 Umbraco 树网络控制器,它允许我们为我们的关键词创建所需的菜单行为,并显示来自我们的数据库关键词表的关键词集合。
[PluginController("Utilities")]
[Umbraco.Web.Trees.Tree("Utilities", "KeyphraseTree", "Keyphrase", iconClosed: "icon-doc", sortOrder: 1)]
public class KeyphraseTreeController : TreeController
{
private KeyphraseApiController _keyphraseApiController;
public KeyphraseTreeController()
{
_keyphraseApiController = new KeyphraseApiController();
}
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
var nodes = new TreeNodeCollection();
var keyphrases = _keyphraseApiController.GetAll();
if (id == Constants.System.Root.ToInvariantString())
{
foreach (var keyphrase in keyphrases)
{
var node = CreateTreeNode(
keyphrase.Id.ToString(),
"-1",
queryStrings,
keyphrase.ToString(),
"icon-book-alt",
false);
nodes.Add(node);
}
}
return nodes;
}
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
{
var menu = new MenuItemCollection();
if (id == Constants.System.Root.ToInvariantString())
{
// root actions
menu.Items.Add<CreateChildEntity, ActionNew>(ui.Text("actions", ActionNew.Instance.Alias));
menu.Items.Add<RefreshNode, ActionRefresh>(ui.Text("actions", ActionRefresh.Instance.Alias), true);
return menu;
}
else
{
menu.Items.Add<ActionDelete>(ui.Text("actions", ActionDelete.Instance.Alias));
}
return menu;
}
}
类装饰器和 TreeController 扩展允许我们为关键字树声明 Web 控制器,将其与 Utilities 自定义部分相关联,以及选择图标和排序顺序。
我们还声明了一个 api 控制器(我们会讲到的!),这将允许我们访问我们的 Keyphrase 数据对象。
GetTreeNodes 方法允许我们迭代关键短语数据集合并将结果节点返回给视图。
GetMenuNode 方法允许我们创建自定义部分所需的菜单选项。
我们声明如果节点是根(Utilities),那么允许我们添加子节点并刷新节点集合。
但是,如果我们在节点树(Keyphrase)中较低,那么我们只希望用户能够删除节点(即不应允许用户创建比 Keyphrase 更深的另一级节点)
-
为我们的 Keyphrase CRUD 请求创建一个 api 控制器
public class KeyphraseApiController : UmbracoAuthorizedJsonController
{
public IEnumerable<Keyphrase> GetAll()
{
var query = new Sql().Select("*").From("keyphrase");
return DatabaseContext.Database.Fetch<Keyphrase>(query);
}
public Keyphrase GetById(int id)
{
var query = new Sql().Select("*").From("keyphrase").Where<Keyphrase>(x => x.Id == id);
return DatabaseContext.Database.Fetch<Keyphrase>(query).FirstOrDefault();
}
public Keyphrase PostSave(Keyphrase keyphrase)
{
if (keyphrase.Id > 0)
DatabaseContext.Database.Update(keyphrase);
else
DatabaseContext.Database.Save(keyphrase);
return keyphrase;
}
public int DeleteById(int id)
{
return DatabaseContext.Database.Delete<Keyphrase>(id);
}
}
使用角度控制器创建自定义剖面视图,这是 Umbraco 7 中的当前架构风格。
需要注意的是,Umbraco 期望您的自定义部分组件放入以下结构 App_Plugins//BackOffice/
我们需要一个视图来显示和编辑我们的关键短语名称、目标短语和 url
<form name="keyphraseForm"
ng-controller="Keyphrase.KeyphraseEditController"
ng-show="loaded"
ng-submit="save(keyphrase)"
val-form-manager>
<umb-panel>
<umb-header>
<div class="span7">
<umb-content-name placeholder=""
ng-model="keyphrase.Name" />
</div>
<div class="span5">
<div class="btn-toolbar pull-right umb-btn-toolbar">
<umb-options-menu ng-show="currentNode"
current-node="currentNode"
current-section="{{currentSection}}">
</umb-options-menu>
</div>
</div>
</umb-header>
<div class="umb-panel-body umb-scrollable row-fluid">
<div class="tab-content form-horizontal" style="padding-bottom: 90px">
<div class="umb-pane">
<umb-control-group label="Target keyphrase" description="Keyphrase to be linked'">
<input type="text" class="umb-editor umb-textstring" ng-model="keyphrase.Phrase" required />
</umb-control-group>
<umb-control-group label="Keyphrase link" description="Internal or external url">
<p>{{keyphrase.Link}}</p>
<umb-link-picker ng-model="keyphrase.Link" required/>
</umb-control-group>
<div class="umb-tab-buttons" detect-fold>
<div class="btn-group">
<button type="submit" data-hotkey="ctrl+s" class="btn btn-success">
<localize key="buttons_save">Save</localize>
</button>
</div>
</div>
</div>
</div>
</div>
</umb-panel>
</form>
这利用 umbraco 和角度标记来动态显示数据输入字段并将我们的视图关联到与我们的数据层交互的角度控制器
angular.module("umbraco").controller("Keyphrase.KeyphraseEditController",
function ($scope, $routeParams, keyphraseResource, notificationsService, navigationService) {
$scope.loaded = false;
if ($routeParams.id == -1) {
$scope.keyphrase = {};
$scope.loaded = true;
}
else {
//get a keyphrase id -> service
keyphraseResource.getById($routeParams.id).then(function (response) {
$scope.keyphrase = response.data;
$scope.loaded = true;
});
}
$scope.save = function (keyphrase) {
keyphraseResource.save(keyphrase).then(function (response) {
$scope.keyphrase = response.data;
$scope.keyphraseForm.$dirty = false;
navigationService.syncTree({ tree: 'KeyphraseTree', path: [-1, -1], forceReload: true });
notificationsService.success("Success", keyphrase.Name + " has been saved");
});
};
});
那么我们需要 html 和相应的 Angular 控制器来实现关键短语删除行为
<div class="umb-pane" ng-controller="Keyphrase.KeyphraseDeleteController">
<p>
Are you sure you want to delete {{currentNode.name}} ?
</p>
<div>
<div class="umb-pane btn-toolbar umb-btn-toolbar">
<div class="control-group umb-control-group">
<a href="" class="btn btn-link" ng-click="cancelDelete()"
<localize key="general_cancel">Cancel</localize>
</a>
<a href="" class="btn btn-primary" ng-click="delete(currentNode.id)">
<localize key="general_ok">OK</localize>
</a>
</div>
</div>
</div>
</div>
-
利用 Umbraco 的链接选择器允许用户选择内部或外部 URL。
我们需要 html 标记来启动 LinkPicker
<div>
<ul class="unstyled list-icons">
<li>
<i class="icon icon-add blue"></i>
<a href ng-click="openLinkPicker()" prevent-default>Select</a>
</li>
</ul>
</div>
还有一个关联的指令 js 文件,它启动链接选择器并将选定的 url 发布回 html 视图
angular.module("umbraco.directives")
.directive('umbLinkPicker', function (dialogService, entityResource) {
return {
restrict: 'E',
replace: true,
templateUrl: '/App_Plugins/Utilities/umb-link-picker.html',
require: "ngModel",
link: function (scope, element, attr, ctrl) {
ctrl.$render = function () {
var val = parseInt(ctrl.$viewValue);
if (!isNaN(val) && angular.isNumber(val) && val > 0) {
entityResource.getById(val, "Content").then(function (item) {
scope.node = item;
});
}
};
scope.openLinkPicker = function () {
dialogService.linkPicker({ callback: populateLink });
}
scope.removeLink = function () {
scope.node = undefined;
updateModel(0);
}
function populateLink(item) {
scope.node = item;
updateModel(item.url);
}
function updateModel(id) {
ctrl.$setViewValue(id);
}
}
};
});
最后一个 js 文件允许我们通过网络发送数据,其中包含每个人最喜欢的 http 动词 GET、POST(也在这里放置句柄)和 DELETE
angular.module("umbraco.resources")
.factory("keyphraseResource", function ($http) {
return {
getById: function (id) {
return $http.get("BackOffice/Api/KeyphraseApi/GetById?id=" + id);
},
save: function (keyphrase) {
return $http.post("BackOffice/Api/KeyphraseApi/PostSave", angular.toJson(keyphrase));
},
deleteById: function (id) {
return $http.delete("BackOffice/Api/KeyphraseApi/DeleteById?id=" + id);
}
};
});
此外,我们需要一个包清单来注册我们的 javascript 行为
{
javascript: [
'~/App_Plugins/Utilities/BackOffice/KeyphraseTree/edit.controller.js',
'~/App_Plugins/Utilities/BackOffice/KeyphraseTree/delete.controller.js',
'~/App_Plugins/Utilities/keyphrase.resource.js',
'~/App_Plugins/Utilities/umbLinkPicker.directive.js'
]
}
实施调整以允许解决方案的 CMS 部分正常工作。
在这一点上,我们几乎已经完成了自定义部分的歌唱,但我们只需要再跳几个 Umbraco 箍,即
a) 添加一个 keyphrase 事件类,如果它不存在则创建我们的 keyphrase db 表(参见第 8 点)
b) 启动 Umbraco 并将新的自定义部分关联到目标用户(从用户菜单)
c) 通过在 umbraco-->config-->en.xml 中搜索自定义部分的占位符文本并将占位符文本替换为“Utilities”
,从而更改自定义部分的占位符文本
在保存或发布内容时拦截目标数据类型的目标内容字段
我的要求是截取新闻文章的正文内容,因此您需要在 Umbraco 中创建一个文档类型,例如,标题字段类型为“Textstring”,bodyContent 字段类型为“Richtext”编辑”。
您还需要一个或多个关键词来定位,现在应该在新的 Umbraco 自定义部分“实用程序”中
在这里,我将关键词“技术新闻”定位为链接到 bbc 技术新闻网站,这样每当我写下“技术新闻”这个短语时,href 链接就会自动插入。
这显然是一个非常简单的示例,但如果用户需要链接到某些重复的法律文件,例如税收、财产、尽职调查,这些文件可以在外部或在 CMS 本身内托管,这将非常强大。 href 链接将在新选项卡中打开外部资源,并在同一窗口中打开内部资源(我们将在第 9 点中讨论)
因此,我们试图实现的原理是拦截文档的 Umbraco 保存事件并操纵我们的富文本以插入我们的链接。这是按如下方式完成的:
a) 建立一个方法 (ContentServiceOnSaving),当用户单击“保存”或“发布并保存”时将触发该方法。
b)定位我们想要的内容字段以找到我们的关键词。
c) 针对我们的关键词集合解析目标内容 html 以创建我们的内部/外部链接。
注意:如果您只想启动并运行自定义部分,则只需要 ApplicationStarted 方法来创建 KeyPhrase 表。
public class KeyphraseEvents : ApplicationEventHandler
{
private KeyphraseApiController _keyphraseApiController;
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication,
ApplicationContext applicationContext)
{
_keyphraseApiController = new KeyphraseApiController();
ContentService.Saving += ContentServiceOnSaving;
var db = applicationContext.DatabaseContext.Database;
if (!db.TableExist("keyphrase"))
{
db.CreateTable<Keyphrase>(false);
}
}
private void ContentServiceOnSaving(IContentService sender, SaveEventArgs<IContent> saveEventArgs)
{
var keyphrases = _keyphraseApiController.GetAll();
var keyphraseContentParser = new KeyphraseContentParser();
foreach (IContent content in saveEventArgs.SavedEntities)
{
if (content.ContentType.Alias.Equals("NewsArticle"))
{
var blogContent = content.GetValue<string>("bodyContent");
var parsedBodyText = keyphraseContentParser.ReplaceKeyphrasesWithLinks(blogContent, keyphrases);
content.SetValue("bodyContent", parsedBodyText);
}
}
}
}
ContentServiceOnSaving 方法允许我们拦截 Umbraco 中的任何保存事件。之后,我们检查我们传入的内容,看看它是否是我们期望的类型——在这个例子中是“NewsArticle”——如果是,然后定位“bodyContent”部分,用我们的“KeyphraseContentParser”解析它,并交换当前'bodyContent' 与解析后的 'bodyContent'。
-
创建一个关键词解析器来交换内部/外部链接的关键词
public class KeyphraseContentParser
{
public string ReplaceKeyphrasesWithLinks(string htmlContent, IEnumerable<Keyphrase> keyphrases)
{
var parsedHtmlStringBuilder = new StringBuilder(htmlContent);
foreach (var keyphrase in keyphrases)
{
if (htmlContent.CaseContains(keyphrase.Phrase, StringComparison.OrdinalIgnoreCase))
{
var index = 0;
do
{
index = parsedHtmlStringBuilder.ToString()
.IndexOf(keyphrase.Phrase, index, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
var keyphraseSuffix = parsedHtmlStringBuilder.ToString(index, keyphrase.Phrase.Length + 4);
var keyPhraseFromContent = parsedHtmlStringBuilder.ToString(index, keyphrase.Phrase.Length);
var keyphraseTarget = "_blank";
if (keyphrase.Link.StartsWith("/"))
{
keyphraseTarget = "_self";
}
var keyphraseLinkReplacement = String.Format("<a href='{0}' target='{1}'>{2}</a>",
keyphrase.Link, keyphraseTarget, keyPhraseFromContent);
if (!keyphraseSuffix.Equals(String.Format("{0}</a>", keyPhraseFromContent)))
{
parsedHtmlStringBuilder.Remove(index, keyPhraseFromContent.Length);
parsedHtmlStringBuilder.Insert(index, keyphraseLinkReplacement);
index += keyphraseLinkReplacement.Length;
}
else
{
var previousStartBracket = parsedHtmlStringBuilder.ToString().LastIndexOf("<a", index);
var nextEndBracket = parsedHtmlStringBuilder.ToString().IndexOf("a>", index);
parsedHtmlStringBuilder.Remove(previousStartBracket, (nextEndBracket - (previousStartBracket - 2)));
parsedHtmlStringBuilder.Insert(previousStartBracket, keyphraseLinkReplacement);
index = previousStartBracket + keyphraseLinkReplacement.Length;
}
}
} while (index != -1);
}
}
return parsedHtmlStringBuilder.ToString();
}
}
单步执行上述代码可能是最简单的,但从根本上说,解析器必须:
a) 使用指向内部 CMS 或外部 Web 资源的链接查找并包装所有关键短语,忽略大小写。
b) 处理已解析的 html 字符串,以保留链接而不创建嵌套链接。
c) 允许在解析的 html 字符串中更新 CMS 关键字更改。
本篇博客,以及github代码可以从上一篇的链接中找到。