В данной статье я опишу процесс создания дерева данных без использования сторонних компонентов. Будут использованы ASP.NET Web API и библиотека AngularJS. Для манипуляции DOM используется чистый код JavaScript (без использования дополнительных библиотек наподобие jQuery), а для доступа к данным – технология ADO.NET (без ORM). Тем самым хочу показать как можно написать простой и понятный код без всяких излишеств.
Для начала создаём пустой проект ASP.NET 4.5.1 в Visual Studio 2013.
Загружаем нужные библиотеки через NuGet.
После установки всех нужных пакетов, файл конфигурации packages.config должен выглядеть так:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AngularJS.Core" version="1.2.16" targetFramework="net451" />
<package id="Microsoft.AspNet.WebApi" version="5.1.2" targetFramework="net451" />
<package id="Microsoft.AspNet.WebApi.Client" version="5.1.2" targetFramework="net451" />
<package id="Microsoft.AspNet.WebApi.Core" version="5.1.2" targetFramework="net451" />
<package id="Microsoft.AspNet.WebApi.Owin" version="5.1.2" targetFramework="net451" />
<package id="Microsoft.AspNet.WebApi.WebHost" version="5.1.2" targetFramework="net451" />
<package id="Microsoft.Owin" version="2.1.0" targetFramework="net451" />
<package id="Microsoft.Owin.Host.SystemWeb" version="2.0.1" targetFramework="net451" />
<package id="Newtonsoft.Json" version="5.0.1" targetFramework="net451" />
<package id="Owin" version="1.0" targetFramework="net451" />
</packages>
Приложение будет построено по спецификации OWIN. Добавим конфигурационный класс
Startup конвейера.
using AngularjsTreeView;
using Microsoft.Owin;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Owin;
using System.Net.Http.Formatting;
using System.Web.Http;
[assembly: OwinStartup(typeof(Startup))]
namespace AngularjsTreeView
{
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
var httpConfiguration = new HttpConfiguration();
httpConfiguration.Formatters.Clear();
httpConfiguration.Formatters.Add(new JsonMediaTypeFormatter());
httpConfiguration.Formatters.JsonFormatter.SerializerSettings =
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
httpConfiguration.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
appBuilder.UseWebApi(httpConfiguration);
}
}
}
Создадим страницу для приложения, добавив разметку. Будет использована простая статическая страница.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Tree page</title>
<script src="scripts/angular.js"></script>
<script src="scripts/application/application.js"></script>
<script src="scripts/application/controllers/treePageController.js"></script>
<script src="scripts/Application/services/treePageService.js"></script>
<script src="scripts/Application/directives/pageTreeDirective.js"></script>
<link href="/Content/Styles/TreePage.css" rel="stylesheet" />
<link href="/Content/Styles/PagesTree.css" rel="stylesheet" />
</head>
< data-ng-app="application" data-ng-controller="treePageController">
<h1>Tree view example</h1>
<div class="treeDiv" data-ng-click="clearSelectedNode()">
<treeview nodes="treePageItem.treeViewPageNodes" temp-data="tempData"
class="treeview" />
</div>
<input type="button" data-ng-click="addNewNode()" class="button"
value="Add node" />
<input type="button" data-ng-click="removeNode()" class="button"
value="Remove node" />
<input type="button" data-ng-click="saveTreeData()" class="button"
value="Save tree data" />
<div data-ng-class="{updprocess: applicationBlocked}"></div>
</>
</html>
Контроллер JavaScript для страницы
applicationModule.controller('treePageController',
function ($scope, treePageService) {
treePageService.get().then(function(treePageItem) {
$scope.treePageItem = treePageItem;
});
$scope.applicationBlocked = false;
$scope.tempData = {};
$scope.tempData.selectedNode = null;
$scope.tempData.adjacentNodes = null;
$scope.clearSelectedNode = function() {
if ($scope.tempData.selectedNode) {
$scope.tempData.selectedNode.isSelected = false;
$scope.tempData.selectedNode = null;
$scope.tempData.adjacentNodes = null;
}
};
$scope.addNewNode = function() {
var nodeName = prompt("Please enter node name.", "Node name");
if (!nodeName) {
alert("Value for node name is not valid.");
}
var rootNodes = $scope.treePageItem.treeViewPageNodes;
if ($scope.tempData.selectedNode) {
rootNodes = $scope.tempData.selectedNode.childNodes;
}
var parentId = $scope.tempData.selectedNode ? $scope.tempData.selectedNode.id : null;
var newNode = {
"isExpanded": false,
"childNodes": [],
"id": null,
"parentId": parentId,
"nodeName": nodeName,
"isSelected": false
}
rootNodes.push(newNode);
};
$scope.removeNode = function() {
if (!$scope.tempData.selectedNode) {
alert("Please select node for remove.");
}
var index = $scope.tempData.adjacentNodes
.indexOf($scope.tempData.selectedNode);
if (index > -1) {
$scope.tempData.adjacentNodes.splice(index, 1);
}
};
$scope.saveTreeData = function () {
$scope.applicationBlocked = true;
treePageService.update($scope.treePageItem.treeViewPageNodes)
.success(function (treeDataMessage) {
if (treeDataMessage.dataProcessedSuccessfully) {
alert("Data saved.");
} else {
alert("Was error on server.");
}
$scope.applicationBlocked = false;
});
};
});
и самое главное в данной статье – код директив для генерации дерева.
applicationModule.directive('treeview', function () {
return {
restrict: "E",
replace: true,
scope: {
nodes: '=',
tempData: '='
},
template: '<ul><node ng-repeat="node in nodes" ' +
'node="node" temp-data="tempData" adjacent-nodes="nodes"></node></ul>'
};
}
);
applicationModule.directive('node', [
'$compile', function ($compile) {
return {
restrict: "E",
replace: true,
scope: {
node: '=',
tempData: '=',
adjacentNodes: '='
},
template: '<li><div ng-if="node.childNodes.length > 0" ' +
'ng-click="expandOrCollapseNode(node);" ' +
'ng-class="{true:\'hitarea collapsable\', ' +
'false:\'hitarea expandable\'}[node.isExpanded]"></div>' +
'<span ng-class="{true: \'selectedNode\'}[node.isSelected]" ' +
'ng-click="nodeClick(node, $event);" temp-data="tempData">' +
'{{node.nodeName}}</span></li>',
controller: [
'$scope', '$element', function ($scope, $element) {
$scope.nodeClick = function (node, event) {
if ($scope.tempData.selectedNode) {
$scope.tempData.selectedNode.isSelected = false;
}
$scope.tempData.selectedNode = node;
$scope.tempData.adjacentNodes = $scope.adjacentNodes;
node.isSelected = true;
event.stopImmediatePropagation();
};
$scope.expandOrCollapseNode = function (pageNode) {
if (pageNode.isExpanded == true) {
pageNode.isExpanded = false;
} else {
pageNode.isExpanded = true;
}
};
}
],
link: function (scope, element, attrs) {
//console.log(angular.isArray(scope.node.childNodes));
if (angular.isArray(scope.node.childNodes)) {
var content = $compile('<treeview ng-if="node.isExpanded"' +
'nodes="node.childNodes" temp-data="tempData"></treeview>')(scope);
element.append(content);
}
}
};
}
]);
Добавим сервис для AJAX-запросов
applicationModule.factory('treePageService', function ($http, $q) {
return {
get: function() {
var deferred = $q.defer();
$http.get('/api/TreePage').success(deferred.resolve).error(deferred.reject);
return deferred.promise;
},
update: function (treeViewPageNodes) {
var request = $http({
method: "post",
url: "/api/TreePage",
data: treeViewPageNodes
});
return request;
}
};
});
и контроллер Web API, который будет отвечать на них.
namespace AngularjsTreeView.Api
{
using DataAccess;
using DomainModel;
using System.Collections.Generic;
using System.Web.Http;
public class TreePageController : ApiController
{
private readonly TreeViewDataService treeViewDataService =
new TreeViewDataService(new TreeViewDataRepository());
public TreePageItem Get()
{
return treeViewDataService.GetTreePageItem();
}
[HttpPost]
public TreeDataMessage Update(IEnumerable<TreeViewPageNode> treeViewPageNodes)
{
return treeViewDataService.Update(treeViewPageNodes);
}
}
}
Создадим код серверной логики приложения
namespace AngularjsTreeView.DomainModel
{
using DataAccess;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
public class TreeViewDataService
{
private readonly ITreeViewDataRepository treeViewDataRepository;
public TreeViewDataService(ITreeViewDataRepository treeViewDataRepository)
{
this.treeViewDataRepository = treeViewDataRepository;
}
public TreePageItem GetTreePageItem()
{
var treePageItem = new TreePageItem();
var rootTreeNode = new TreeViewPageNode();
FillPageTreeNode(rootTreeNode, treeViewDataRepository.GetAllTreeViewNodes());
treePageItem.TreeViewPageNodes = rootTreeNode.ChildNodes;
return treePageItem;
}
public TreeDataMessage Update(IEnumerable<TreeViewPageNode> treeViewPageNodes)
{
var message = new TreeDataMessage() { DataProcessedSuccessfully = true };
var treeViewNodes = new List<TreeViewNode>();
FillPageTreeDataForUpdate(treeViewPageNodes.ToList(), treeViewNodes);
treeViewDataRepository.Update(treeViewNodes);
return message;
}
private void FillPageTreeNode(TreeViewPageNode parentTreeNode,
IList<TreeViewNode> treeViewNodes)
{
int? parentId = null;
if (parentTreeNode.Id > 0)
parentId = parentTreeNode.Id;
parentTreeNode.ChildNodes = treeViewNodes
.Where(node => parentId == node.ParentId).Select(node =>
{
var pageTreeNode = new TreeViewPageNode()
{
Id = node.Id,
ParentId = node.ParentId,
NodeName = node.NodeName,
IsExpanded = false,
};
return pageTreeNode;
}).ToList();
foreach (var treeNode in parentTreeNode.ChildNodes)
{
FillPageTreeNode(treeNode, treeViewNodes);
}
}
private void FillPageTreeDataForUpdate(IList<TreeViewPageNode> treeViewPageNodes,
List<TreeViewNode> treeViewNodes)
{
foreach (var treeViewPageNode in treeViewPageNodes)
{
Trace.WriteLine(treeViewPageNode.Id);
if (treeViewPageNode.ChildNodes != null && treeViewPageNode.ChildNodes.Any())
{
FillPageTreeDataForUpdate(treeViewPageNode.ChildNodes.ToList(), treeViewNodes);
}
treeViewNodes.Add(treeViewPageNode);
}
}
}
}
и код доступа к хранилищу.
namespace AngularjsTreeView.DataAccess
{
using DomainModel;
using Microsoft.SqlServer.Server;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
public class TreeViewDataRepository : RepositoryBase, ITreeViewDataRepository
{
#region ITreeViewDataRepository Members
public IList<TreeViewNode> GetAllTreeViewNodes()
{
var sqlConnection = new SqlConnection(ConnectionString);
var sqlCommand = new SqlCommand("dbo.GetTreeViewData", sqlConnection)
{
CommandType = CommandType.StoredProcedure,
};
var resultList = new List<TreeViewNode>();
try
{
sqlConnection.Open();
using (SqlDataReader reader = sqlCommand.ExecuteReader())
{
while (reader.Read())
{
var item = new TreeViewNode();
item.Id = int.Parse(reader["Id"].ToString());
item.ParentId = reader["ParentId"].TryParseToInt();
item.NodeName = reader["NodeName"].ToString();
resultList.Add(item);
}
}
}
finally
{
sqlConnection.Close();
}
return resultList;
}
public void Update(IList<TreeViewNode> treeViewNodes)
{
var sqlConnection = new SqlConnection(ConnectionString);
var sqlCommand = new SqlCommand("dbo.UpdateTreeViewData", sqlConnection)
{
CommandType = CommandType.StoredProcedure,
};
sqlCommand.Parameters.Add("@p_TreeViewData", SqlDbType.Structured);
var list = new List<SqlDataRecord>();
foreach (var treeViewNode in treeViewNodes)
{
var sqlDataRecord = new SqlDataRecord(
new SqlMetaData("Id", SqlDbType.Int),
new SqlMetaData("ParentId", SqlDbType.Int),
new SqlMetaData("NodeName", SqlDbType.NVarChar, 50),
new SqlMetaData("IsSelected", SqlDbType.Bit));
sqlDataRecord.SetValue(0, treeViewNode.Id);
sqlDataRecord.SetValue(1, treeViewNode.ParentId);
sqlDataRecord.SetString(2, treeViewNode.NodeName);
sqlDataRecord.SetValue(3, treeViewNode.IsSelected);
list.Add(sqlDataRecord);
}
sqlCommand.Parameters["@p_TreeViewData"].Value = list;
try
{
sqlConnection.Open();
sqlCommand.ExecuteNonQuery();
}
finally
{
sqlConnection.Close();
}
}
#endregion
}
}
В качестве хранилища используется SQL Server LocalDB.
Используется одна таблица и пара процедур.
CREATE PROCEDURE [dbo].[GetTreeViewData]
AS
SELECT * FROM dbo.TreeViewData
RETURN 0
CREATE PROCEDURE [dbo].[UpdateTreeViewData]
@p_TreeViewData udtt_TreeViewData READONLY
AS
BEGIN
MERGE TreeViewData AS target
USING(SELECT * FROM @p_TreeViewData) AS SOURCE
ON target.Id = SOURCE.Id
WHEN MATCHED THEN
UPDATE SET target.ParentId = SOURCE.ParentId
WHEN NOT MATCHED BY TARGET AND SOURCE.Id IS NULL THEN
INSERT(ParentId, NodeName)
VALUES(SOURCE.ParentId, SOURCE.NodeName)
WHEN NOT MATCHED BY SOURCE THEN DELETE;
END
Остальные мелочи не буду приводить, дабы не загромождать код. Осталось всё это запустить и увидеть.
Готовый код можно
скачать отсюда.