サーバー上のファイルをブラウザ上にツリー表示してくれる「jQueryFileTree」を試してみました。
jQuery File Tree
http://jqueryfiletree.github.io/
表示先のルートフォルダを指定すれば、その配下のフォルダやファイルを簡単にツリー表示してくれます。フォルダ・ファイル情報の取得は、1階層ごとに非同期通信で取得します。つまり、Web画面表示時には、最上位のフォルダ情報のみ取得し、その配下の情報は取得しません。あるフォルダをクリックしたら、そのフォルダの配下の情報のみ取得しますので、画面表示時に無駄な情報を取得する時間はかかりません。
ソースはこちらから取得できます。
https://github.com/jqueryfiletree/jqueryfiletree
#概要
jQuery File Treeの概要を簡単に図示します。
サーバー側のファイル・フォルダ情報を表示するものなので、サーバー側にその情報を取得するプログラムが必要です。このサーバー側のプログラムが、ブラウザ側のフォルダクリック時に発行される非同期通信を受け、その1階層下の情報を取得し、ブラウザ側に返すようになっています。
このサーバー側のプログラムには、PHP用、Java用など、多数用意されています。今回はASP.NET MVC用のプログラムを利用してみます。
#早速使ってみる
Visual StudioでASP.NET MVCのソリューションを作成します。ここに、ダウンロードしてきたjQueryFileTreeのプログラムを配置していきます。この記事を投稿した段階では、NuGetでのインストールができないようなので、手動でやっていきます。
JavaScriptファイルとCSSファイル、アイコンイメージの配置
まず、jQueryFileTree.jsの配置です。今回は下図のようにScriptフォルダ直下に配置しました。実際の運用には、jQueryFileTree.min.jsを使うほうが良いと思いますが、今回は使いません(というのも、後でソースの修正を行うからです)。
次にjQueryFileTree.cssとアイコンイメージのファイルです。それぞれ、Contentフォルダ配下に配置します。CSSファイルも、jQueryFileTree.min.cssがあるので、実際の運用にはこちらを使った方が良いと思います。
これらのファイルの参照設定を、_Layout.cshtmlファイルにします。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - マイ ASP.NET アプリケーション</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
<!-- jQueryFileTreeのcssを追加 -->
<link href="/Content/jQueryFileTree.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@Html.ActionLink("アプリケーション名", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>@Html.ActionLink("ホーム", "Index", "Home")</li>
<li>@Html.ActionLink("詳細", "About", "Home")</li>
<li>@Html.ActionLink("連絡先", "Contact", "Home")</li>
</ul>
@Html.Partial("_LoginPartial")
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© @DateTime.Now.Year - マイ ASP.NET アプリケーション</p>
</footer>
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
<!-- jQueryFileTree.jsを追加 -->
<script src="/Scripts/jQueryFileTree.js"></script>
</body>
</html>
##モデルとコントロールの配置
ダウンロードしてきたファイル中の "jqueryfiletree-master\dist\connectors\Asp.Net-MVC" フォルダに、ASP.NET MVC用のファイル・フォルダ取得プログラム、およびそのモデルのプログラムが入っています。これをVisual Studioに配置していきます。
まずはモデルです。下図のように、FileTreeViewMode.csをVisual Studioに追加します。
このFileTreeViewMode.csですが、namespaceの定義が入っていないので、以下のようにnamespaceを追加しておきます。
namespace FileTreeTest.Models // ← namespaceの追加をしておきます。
{
public class FileTreeViewModel
{
public string Name { get; set; }
public string Ext { get; set; }
public string Path { get; set; }
public bool IsDirectory { get; set; }
public string PathAltSeparator()
{
return Path.Replace("\\", "/");
}
}
}
次にコントローラです。jQueryFileTreeで提供されている FileTreeController.cs のプログラムを、今回はお試しということで、 HomeController.cs にコピーします(実際には各システム応じて適した箇所に配置してください)。
このプログラムを見てもらうと分かる通り、基底フォルダ "baseDir" 配下のファイル・フォルダしか見せないようになっています。本来のシステムではこうあるべきなのですが、今回はjQueryFileTreeの動きを分かりやすく示したいので、セキュリティ上あまり良くないのですが、以下のように修正しました。
[HttpPost]
public virtual ActionResult GetFiles(string dir)
{
// 基底フォルダ+相対パスでアクセス先を設定する元々のプログラムはコメントアウト。
//const string baseDir = @"/App_Data/userfiles/";
//dir = Server.UrlDecode(dir);
//string realDir = Server.MapPath(baseDir + dir);
////validate to not go above basedir
//if (!realDir.StartsWith(Server.MapPath(baseDir)))
//{
// realDir = Server.MapPath(baseDir);
// dir = "/";
//}
// 受け取ったフォルダパスをURLデコードのみして、そのまま使う。
string realDir = Server.UrlDecode(dir).Replace("/", "\\");
List<FileTreeViewModel> files = new List<FileTreeViewModel>();
DirectoryInfo di = new DirectoryInfo(realDir);
foreach (DirectoryInfo dc in di.GetDirectories())
{
files.Add(new FileTreeViewModel() { Name = dc.Name, Path = String.Format("{0}\\{1}\\", realDir, dc.Name), IsDirectory = true });
//↑【注意】{0}と{1}の間に"\\"を入れておきます。
}
foreach (FileInfo fi in di.GetFiles())
{
files.Add(new FileTreeViewModel() { Name = fi.Name, Ext = fi.Extension.Substring(1).ToLower(), Path = realDir + fi.Name, IsDirectory = false });
}
return PartialView(files);
}
##GetFiles.cshtmlの追加とIndex.cshtmlの修正
jQueryFileTreeでは、サーバーから受け取ったファイル・フォルダ情報を、一度<UL>&<LI>タグでツリー情報を展開します。そのためのプログラムとして GetFiles.cshtml が用意されています。このファイルを、Views/Homeフォルダに追加します。
追加した後に、以下に示したように using を忘れずに追加してください。
@using FileTreeTest.Models <!-- ← この@usingを追加してください。 -->
@model IEnumerable<FileTreeViewModel>
<ul class="jqueryFileTree">
@foreach (var item in Model)
{
if (item.IsDirectory) {
<li class="directory collapsed">
<a href="#" rel="@item.PathAltSeparator()">@item.Name</a>
</li>
}
else
{
<li class="file ext_@item.Ext">
<a href="#" rel="@item.PathAltSeparator()">@item.Name</a>
</li>
}
}
</ul>
今回のお試しでは、Home/Index.cshtmlにファイルツリーを表示させようとおもいますので、Index.cshtmlを修正します。ちょっと長いですが、以下が修正したソースコードです。修正箇所をコメントで書いておきました。
<!-- 以下の2行を追加します -->
@using FileTreeTest.Models
@model IEnumerable<FileTreeViewModel>
@{
ViewBag.Title = "Home Page";
}
<!-- 元からあったサンプルHTMLはコメントアウトします。
<div class="jumbotron">
<h1>ASP.NET</h1>
<p class="lead">ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.</p>
<p><a href="http://asp.net" class="btn btn-primary btn-lg">Learn more »</a></p>
</div>
<div class="row">
<div class="col-md-4">
<h2>Getting started</h2>
<p>
ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
enables a clean separation of concerns and gives you full control over markup
for enjoyable, agile development.
</p>
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301865">Learn more »</a></p>
</div>
<div class="col-md-4">
<h2>Get more libraries</h2>
<p>NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.</p>
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301866">Learn more »</a></p>
</div>
<div class="col-md-4">
<h2>Web Hosting</h2>
<p>You can easily find a web hosting company that offers the right mix of features and price for your applications.</p>
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301867">Learn more »</a></p>
</div>
</div>
-->
<-- これより下の行を全て追加してください -->
@{
if (Model != null)
{
Html.RenderPartial("~/Views/Home/GetFiles.cshtml", Model);
}
}
<h1>JQueryFileTreeテスト</h1>
<div class="row">
<div class="col-md-6">
<div id="container_id"></div>
</div>
<div class="col-md-6">
<label id="selectedItem"></label>
</div>
</div>
<script type="text/javascript">
window.onload = function () {
$('#container_id').fileTree({
root: 'C:\\Work', //ここに、表示先のフォルダを指定します。
script: '/Home/GetFiles',
expandSpeed: 100,
collapseSpeed: 100,
multiFolder: false,
onlyFolders: false
},
function (file) {
$('#selectedItem').html(file);
});
};
</script>
以上で準備完了です。これでVisual Studioからデバッグ実行すると、以下のような画面が表示されます。フォルダをクリックすれば、その配下の情報が取得されます。
#フォルダのみ表示させる
上記のプログラムを見ると、jQueryFileTreeのコンストラクタ "fileTree()" 中に、onlyFoldersというパラメータがあることが分かります。これをtrueにすればフォルダのみを表示することができるはずです。ですが、実際にここをtrueにして実行しても、ファイルは表示されてしまいます。
この原因を調査するために、jQueryFileTreeのソースコードを見てみましょう。下図に示す部分が、フォルダクリック時に非同期通信を実行する部分です。
$.ajax()の実行により非同期通信の処理が走るのが分かります。そして、この data に、サーバー側に渡すパラメータが入っています。この data は、以下の部分に記述されています。
見てわかる通り、onlyFoldersパラメータをサーバー側に渡していることが分かります。ところが、この非同期通信の送信先となる、先ほどHomeController.csに追加したGetFiles()メソッドには、引数dirしかありません。つまりdir以外のパラメータを受け取っていなかったのです。これが原因でonlyFoldersパラメータがうまく動かなかったのです(つまり、GitHub上で提供されているASP.NET MVCプログラムの不備です)。
そこで、以下のようにonlyFoldersを追加します。ついでにonlyFilesも追加しましょう。以下のようにソースを修正します。ASP.NET MVCのルールで、非同期通信で渡すパラメータ名と引数の変数名が一致していないと正しく受け取れませんので、変数名は、jQueryFileTree.csからコピー&ペーストした方が良いと思います。
[HttpPost]
public virtual ActionResult GetFiles(string dir, bool onlyFolders, bool onlyFiles)
{
// 受け取ったフォルダパスをURLデコードのみして、そのまま使う。
string realDir = Server.UrlDecode(dir).Replace("/", "\\");
List<FileTreeViewModel> files = new List<FileTreeViewModel>();
DirectoryInfo di = new DirectoryInfo(realDir);
if (!onlyFiles)
{
foreach (DirectoryInfo dc in di.GetDirectories())
{
files.Add(new FileTreeViewModel() { Name = dc.Name, Path = String.Format("{0}\\{1}\\", realDir, dc.Name), IsDirectory = true });
//↑【注意】{0}と{1}の間に"\\"を入れておきます。
}
}
if (!onlyFolders)
{
foreach (FileInfo fi in di.GetFiles())
{
files.Add(new FileTreeViewModel() { Name = fi.Name, Ext = fi.Extension.Substring(1).ToLower(), Path = realDir + fi.Name, IsDirectory = false });
}
}
return PartialView(files);
}
以上の修正をした後で再び実行すると、フォルダのみがツリーに表示されるようになります。
#ルートフォルダを表示させる方法
上記の手順で一応サーバー上のファイル・フォルダは表示できますが、見てわかる通り、ルートフォルダが表示されていません。fileTree()内のパラメータ "root" で指定したフォルダ配下の情報がツリーのトップノードに表示されています。ルートフォルダを表示させるには、一工夫必要です。
jQueryFileTree.js側の修正
非同期通信でサーバー側に渡すパラメータを1つ増やします。何を増やすかというと、rootフォルダのパスです。先ほど説明した data 変数に、以下の項目を追加します。
FileTree.prototype.showTree = function(el, dir, finishCallback) {
var $el, _this, data, handleFail, handleResult, options, result;
$el = $(el);
options = this.options;
_this = this;
$el.addClass('wait');
$(".jqueryFileTree.start").remove();
data = {
dir: dir,
onlyFolders: options.onlyFolders,
onlyFiles: options.onlyFiles,
multiSelect: options.multiSelect,
rootFolders: options.root // この行を追加する。
};
これで、サーバー側のGetFiles()メソッドに、rootフォルダのパスが送られます。後はGetFiles()メソッドを以下のように修正します。
[HttpPost]
public virtual ActionResult GetFiles(string dir, bool onlyFolders, bool onlyFiles, string rootFolders) //引数"rootFolders"を追加
{
// 受け取ったフォルダパスをURLデコードのみして、そのまま使う。
string realDir = Server.UrlDecode(dir).Replace("/", "\\");
List<FileTreeViewModel> files = new List<FileTreeViewModel>();
DirectoryInfo di = new DirectoryInfo(realDir);
// 【今回追加した部分】表示するフォルダがルートフォルダの時には、配下の情報を返さず、ルートフォルダの情報のみ返す
if (realDir == rootFolders)
{
files.Add(new FileTreeViewModel() { Name = di.Name, Path = String.Format("{0}\\", realDir), IsDirectory = true });
return PartialView(files);
}
if (!onlyFiles)
{
foreach (DirectoryInfo dc in di.GetDirectories())
{
files.Add(new FileTreeViewModel() { Name = dc.Name, Path = String.Format("{0}\\{1}\\", realDir, dc.Name), IsDirectory = true });
//↑【注意】{0}と{1}の間に"\\"を入れておきます。
}
}
if (!onlyFolders)
{
foreach (FileInfo fi in di.GetFiles())
{
files.Add(new FileTreeViewModel() { Name = fi.Name, Ext = fi.Extension.Substring(1).ToLower(), Path = realDir + fi.Name, IsDirectory = false });
}
}
return PartialView(files);
}
以上の修正を加えて実行してみると、下図のようにルートフォルダが表示されるようになります。
#ルートフォルダの複数表示
jQueryFileTreeのパラメータ設定部分を以下のようにすることで、複数のルートフォルダが表示されるようにできるとより便利です。
<script type="text/javascript">
window.onload = function () {
$('#container_id').fileTree({
//root: 'C:\\Work',
root: ['C:\\Work', 'C:\\cygwin64'], // このように複数のルートフォルダを指定
script: '/Home/GetFiles',
expandSpeed: 100,
collapseSpeed: 100,
multiFolder: false,
onlyFolders: true
},
function (file) {
$('#selectedItem').html(file);
});
};
</script>
これも若干ソースを修正すれば実現できます。
##jQueryFileTree.js側の修正
まずはjQueryFileTree.js側の修正です。以下に示すように、$.ajax()メソッドに引数 traditional を追加しています。このパラメータをtrueに設定します。
~~ ソースが長いので中略 ~~
if (typeof options.script === 'function') {
result = options.script(data);
if (typeof result === 'string' || result instanceof jQuery) {
return handleResult(result);
} else {
return handleFail();
}
} else {
return $.ajax({
url: options.script,
traditional: true, // ← この行を追加する。
type: 'POST',
dataType: 'HTML',
data: data
}).done(function(result) {
return handleResult(result);
}).fail(function() {
return handleFail();
});
}
};
このパラメータをtrueにする理由は、以下のページを参照してください。
$.ajax で ASP.NET MVC のアクションメソッドに配列を渡せない!?
続・$.ajax で ASP.NET MVC のアクションメソッドに配列を渡す
このページで紹介されているように、traditionalをtrueにしないと、配列のデータをGetFiles()メソッドに渡せないようです。私も最初、この設定をしないで試してみましたが、GetFiles()メソッド側の引数はnullになってしまいました。
##HomeController.cs側の修正
上記のjQueryFileTree.js側の修正が終わったら、HomeController.cs側の修正です。GetFiles()メソッドを以下のように修正します。先ほどの GetFiles() から色々変わっていますので見比べてみてください。
[HttpPost]
public virtual ActionResult GetFiles(string dir, bool onlyFolders, bool onlyFiles, string[] rootFolders) // ←rootFoldersの引数をstring型からstring配列型に変更
{
List<FileTreeViewModel> files = new List<FileTreeViewModel>();
// index.cshtml中で指定されたルートフォルダをList<string>型インスタンスに保持させます。
foreach (string root in rootFolders)
{
string rootDecoded = Server.UrlDecode(root).Replace("/", "\\");
if (!rootList.Contains(rootDecoded))
rootList.Add(rootDecoded);
}
// Web画面の初回表示時に、dirに、2つ設定したルートフォルダの情報が渡されるので、その情報をList<string>インスタンスに変換して保持する。
// (なお、フォルダ・ファイルのクリック時には、そのクリックしたノードの情報のみ渡される。
// 2つのノード情報がカンマ区切りで送られてくるのは初回表示時のみ)
dir = Server.UrlDecode(dir);
List<string> dirList = dir.Replace("/", "\\").Split(delimiter).ToList<string>();
foreach (string realDir in dirList)
{
DirectoryInfo di = new DirectoryInfo(realDir);
// ルートノードと同じフォルダに対する操作に対しては、そのフォルダ配下の情報は返さないようにする。
if (rootList.Contains(realDir))
{
files.Add(new FileTreeViewModel() { Name = di.Name, Path = String.Format("{0}\\", realDir), IsDirectory = true });
continue;
}
if (!onlyFiles)
{
foreach (DirectoryInfo dc in di.GetDirectories())
{
files.Add(new FileTreeViewModel() { Name = dc.Name, Path = String.Format("{0}\\{1}\\", realDir, dc.Name), IsDirectory = true });
}
}
if (!onlyFolders)
{
foreach (FileInfo fi in di.GetFiles())
{
files.Add(new FileTreeViewModel() { Name = fi.Name, Ext = fi.Extension.Substring(1).ToLower(), Path = realDir + fi.Name, IsDirectory = false });
}
}
}
return PartialView(files);
}
ソースを見てもらえれば分かると思いますが、複数のルートフォルダ、複数の選択ノードに対応したループ処理が入ったことが大きな違いです。
ソースのコメント中にも書きましたが、複数の選択ノードがカンマ区切りで渡されるのは、Web画面の初回表示時のみです。一度表示されたツリーのノードクリック時には、今までと同様に、そのクリックされたノードのパスのみが渡されます。
以上の修正を施せば、複数のルートノードの表示ができるようになります。
#フォルダクリック時のパスの取得
jQueryFileTreeでは、ファイルのクリック時には、そのファイルのパスを取得することができます。index.cshtml中の以下で指定したメソッドがファイルクリック時に実行され、その引数にファイルパスが渡されるからです。
<script type="text/javascript">
window.onload = function () {
$('#container_id').fileTree({
root: ['C:\\Work', 'C:\\cygwin64'],
script: '/Home/GetFiles',
expandSpeed: 100,
collapseSpeed: 100,
multiFolder: false,
onlyFolders: true
},
// ファイルノードクリック時に、このメソッドが実行され、引数fileにはクリックされたファイルノードのパスが渡される。
function (file) {
$('#selectedItem').html(file);
});
};
</script>
フォルダクリック時にも同様にフォルダパスを取得したいときがあります。これについても、若干のソース修正で実現できます。
##jQueryFileTree.js側の修正
フォルダクリック時に実行されるプログラム、ファイルクリック時に実行されるプログラムを以下に図示します。先ほど紹介したファイルノードクリック時に実行されるメソッドの呼び元も示しておきました。
上図に示したメソッドの呼び元となる行を、フォルダクリック時の処理に入れてやれば、フォルダクリック時にもフォルダパスが取得できるようになります。ただし、フォルダがクリックされたのかファイルがクリックされたのかの判別がつくように、"isFolder" という引数を追加するようにしましょう。フォルダがクリックされたら true、ファイルがクリックされたら false が渡される引数です。
以下が修正したコードです。
~~ 中略 ~~
if ($ev.parent().hasClass('directory')) {
// ここに以下の1行を追加
typeof callback === "function" ? callback($ev.attr('rel'), true) : void 0;
//↑この引数を追加する。
if ($ev.parent().hasClass('collapsed')) {
if (!options.multiFolder) {
$ev.parent().parent().find('UL').slideUp({
duration: options.collapseSpeed,
easing: options.collapseEasing
});
$ev.parent().parent().find('LI.directory').removeClass('expanded').addClass('collapsed');
}
$ev.parent().removeClass('collapsed').addClass('expanded');
$ev.parent().find('UL').remove();
return _this.showTree($ev.parent(), $ev.attr('rel'), function() {
_this._trigger('filetreeexpanded', _this.data);
return callback != null;
});
} else {
return $ev.parent().find('UL').slideUp({
duration: options.collapseSpeed,
easing: options.collapseEasing,
start: function() {
return _this._trigger('filetreecollapse', _this.data);
},
complete: function() {
$ev.parent().removeClass('expanded').addClass('collapsed');
_this._trigger('filetreecollapsed', _this.data);
return callback != null;
}
});
}
} else {
if (!options.multiSelect) {
jqft.container.find('li').removeClass('selected');
$ev.parent().addClass('selected');
} else {
if ($ev.parent().find('input').is(':checked')) {
$ev.parent().find('input').prop('checked', false);
$ev.parent().removeClass('selected');
} else {
$ev.parent().find('input').prop('checked', true);
$ev.parent().addClass('selected');
}
}
_this._trigger('filetreeclicked', _this.data);
return typeof callback === "function" ? callback($ev.attr('rel'), false) : void 0;
//↑この引数を追加する。
}
そしてindex.cshtmlのコールバック側を以下のように変えます。
<script type="text/javascript">
window.onload = function () {
$('#container_id').fileTree({
root: ['C:\\Work', 'C:\\cygwin64'],
script: '/Home/GetFiles',
expandSpeed: 100,
collapseSpeed: 100,
multiFolder: false,
onlyFolders: false
},
// "isFolder"という名前で引数を追加
function (file, isFolder) {
var message = isFolder ? "フォルダがクリックされました。" : "ファイルがクリックされました。";
$('#selectedItem').html(message + " : " + file);
});
};
</script>
この修正により、フォルダをクリックしたときにもコールバックメソッドが実行され、フォルダパスを引数で受け取ることができるようになります。
以上が、今回のjQueryFileTreeの動作確認で調査した内容です。ソースを修正しないでそのまま使った場合、若干かゆいところに手が届かないような感じの動きでしたが、少しのソース修正で大分良くなるプログラムだと思いました。