読者です 読者をやめる 読者になる 読者になる

1.21 jigowatts

Great Scott!

ASP.NET MVCはじめました~権限による多階層ドロップダウンメニューの表示切り替え

概要

前回の続きというか、前回忘れていた権限によるメニューの表示切り替えです。

環境

権限項目の追加

右端のRoleId項目を追加しました。権限の種類(サンプルではユーザ、チョットできるユーザ、管理者の3種類)ごとに値を設定しておきます。あとはログインユーザごとの権限でメニューの項目を取得してくればOK。

f:id:sh_yoshida:20170329103343p:plain

/Models/MenuItem.cs

public enum Role
{
    User,
    PowerUser,
    Administrator,
}

public class Navigation
{
    public Navigation() : this(role: Role.User)
    {

    }

    public Navigation(Role role)
    {
        this.RoleId = (int)role;
    }

    public int RoleId { get; }

    public IEnumerable<NavigationLink> Menu
    {
        get
        {
            var menu = new List<NavigationLink>();
            using (var context = new AppDbContext())
            {
                var data = context.MenuItems
                    .Where(n => n.RoleId <= RoleId)
                    .OrderBy(n => n.Order)
                    .ToList();

                var mainMenu = data.Where(n => n.ParentId == null);
                foreach (var item in mainMenu)
                {
                    NavigationLink linkItem = MappingLinkItems(item);
                    menu.Add(linkItem);
                }
                SetChildMenu(menu, data);
            }
            return menu;
        }
    }
    #region helper method...
}

実行結果

User権限の場合

RoleIdが"0"以下の項目のみなので、メインメニューの3つだけ。

f:id:sh_yoshida:20170329103348p:plain

Power User権限の場合

RoleIdが"1"以下の項目のみで、3階層分の一部だけ。

f:id:sh_yoshida:20170329103352p:plain

Administrator権限の場合

RoleIdが"2"以下の項目のみ、つまり全部表示。

f:id:sh_yoshida:20170329103356p:plain

ソース

github.com

以上、前回の仕様漏れの実装でした。
sh-yoshida.hatenablog.com

ASP.NET MVCはじめました~データベースで管理する BootStrap 多階層ドロップダウンメニュー

概要

ログインユーザの権限などで表示項目が変わるような動的なメニューが欲しかったので作ってみました。2階層くらい表現できれば十分かとも思ったのですが、多階層ドロップダウンメニューにチャレンジしてみます。

f:id:sh_yoshida:20170328155557g:plain

環境

多階層ドロップダウンメニューの作成

まずはBootStrapで多階層のドロップダウンメニューを実装してみました。CSSこちらから拝借。CSSとViewを書くだけで実装できました。この時点ではリンクリストは直書き。

/Views/Shared/_Layout.cshtml

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
        <li class="dropdown">
            <a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button">
                サンプル<span class="caret"></span>
            </a>
            <ul class="dropdown-menu" role="menu">
                <li>@Html.ActionLink("Upload", "Upload", "Home")</li>
                <li>@Html.ActionLink("Autocomplete", "Create", "Home")</li>
                <li><a href="#">リンクのリスト1</a></li>
                <li><a href="#">リンクのリスト2</a></li>
                <li><a href="#">リンクのリスト3</a></li>
                <li class="dropdown-submenu">
                    <a tableindex="-1" href="#">More options</a>
                    <ul class="dropdown-menu">
                        <li><a href="http://google.com">Google</a></li>
                        <li><a href="http://yahoo.com">Yahoo</a></li>
                    </ul>
                </li>
            </ul>
        </li>
    </ul>
</div>

3階層までだとこんな感じに。固定でよければ簡単に実装できますね!

f:id:sh_yoshida:20170328105433p:plain

データベースよりメニューデータを取得

動的なメニューを実現するにはメンテナンス的にもデータベースでデータを持ってた方がいいんじゃないのってことで、ParentIdによる階層関係のデータを持ったテーブルを用意してみました。これで何階層でも行ける↑↑↑あとはビューヘルパーのActionLinkメソッドを使いたいのでリンクテキストやらアクションメソッドと並び順で構成。

f:id:sh_yoshida:20170328160548p:plain

これらのデータを一旦全部取得して、再帰関数で階層構造データになるように再構成。LINQで一発でできそうな気がすると思ったのですが、無念…。いつかちゃんと調べる。

/Models/MenuItem.cs

public class MenuItem
{
    [Key]
    public int Id { get; set; }
    public int? ParentId { get; set; }
    [Required]
    public string LinkText { get; set; }
    public string ActionName { get; set; }
    public string ControllerName { get; set; }
    public int Order { get; set; }
}

public class NavigationLink : MenuItem
{
    public IEnumerable<NavigationLink> ChildMenu { get; set; }
}

public class Navigation
{
    public IEnumerable<NavigationLink> Menu
    {
        get
        {
            var menu = new List<NavigationLink>();
            using (var context = new AppDbContext())
            {
                var data = context.MenuItems
                    .OrderBy(n => n.Order)
                    .ToList();

                var mainMenu = data.Where(n => n.ParentId == null);
                foreach (var item in mainMenu)
                {
                    NavigationLink linkItem = MappingLinkItems(item);
                    menu.Add(linkItem);
                }
                SetChildMenu(menu, data);
            }
            return menu;
        }
    }

    #region helper method...
}

カスタムビューヘルパーで描画

TagBuilderを使ったカスタムビューヘルパーを作ってみたのですが、思ってたより複雑になったのでサブメニュー部分だけ実装したらいまいちな感じになってしまいました。本当はこれもヘルパー一発で描画したかった。

/Helper/NavigationMenuHelper.cs

public static class NavigationMenuHelper
{
    public static IHtmlString NavigationSubmenuListFor(this HtmlHelper helper, IEnumerable<NavigationLink> submenu)
    {
        if (submenu == null)
        {
            throw new ArgumentNullException("submenu", "submenu is null.");
        }
        var sb = new StringBuilder();
        foreach (var item in submenu)
        {
            if (item.ChildMenu == null)
            {
                var li = new TagBuilder("li");
                li.InnerHtml = helper.ActionLink(item.LinkText, item.ActionName, item.ControllerName).ToString();
                sb.Append(li.ToString(TagRenderMode.Normal));
            }
            else
            {
                sb.Append(CreateSubMenu(helper, item.LinkText, item.ChildMenu));
            }
        }

        return MvcHtmlString.Create(sb.ToString());
    }

    private static string CreateSubMenu(HtmlHelper helper, string linkText, IEnumerable<NavigationLink> subMenu)
    {
        var a = new TagBuilder("a");
        var attributes = new Dictionary<string, string>
        {
            ["tableindex"] = "-1",
            ["href"] = "#",
        };
        a.MergeAttributes(attributes);
        a.InnerHtml = linkText;

        var top_li = new TagBuilder("li");
        top_li.MergeAttribute("class", "dropdown-submenu");
        top_li.InnerHtml = a.ToString();

        var ul = new TagBuilder("ul");
        ul.MergeAttribute("class", "dropdown-menu");

        foreach (var item in subMenu)
        {
            if (item.ChildMenu == null)
            {
                var li = new TagBuilder("li");
                li.InnerHtml = helper.ActionLink(item.LinkText, item.ActionName, item.ControllerName).ToString();
                ul.InnerHtml += li.ToString();
            }
            else
            {
                ul.InnerHtml += CreateSubMenu(helper, item.LinkText, item.ChildMenu);
            }
        }

        top_li.InnerHtml += ul.ToString();
        return top_li.ToString();
    }
}

/Views/Shared/_Layout.cshtml

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        @foreach (var item in Session["menu"] as List<NavigationLink>)
        {
            if (item.ChildMenu == null)
            {
                <li>@Html.ActionLink(item.LinkText, item.ActionName, item.ControllerName)</li>
            }
            else
            {
                <li class="dropdown">
                    <a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-expanded="false">
                        @item.LinkText
                        <span class="caret"></span>
                    </a>
                    <ul class="dropdown-menu" role="menu">
                        @Html.NavigationSubmenuListFor(item.ChildMenu)
                    </ul>
                </li>
            }
        }
    </ul>
</div>

実行結果

一応できました!4階層になるようなデータで実施。これ以上深くしても使いにくそう…。
ぺろ…ッ!!。oO(権限で表示を切り替えようとしてたの忘れてた)

f:id:sh_yoshida:20170328155557g:plain

ソース

github.com

NLogを使ってみる

概要

ログ出力する際にNLogがよさそうなのでTutorialを少しやってみます。
http://nlog-project.org/

環境

Visual Studio Community 2015

インストール

パッケージマネージャコンソールより以下のコマンドを実行するとインストールできます。

PM> Install-Package NLog.Config

ファイルとコンソールに出力

コンソールアプリケーションでログ出力

ログレベルごとにログメッセージを書き込みます。

using NLog;
using System;

namespace NLogConsole
{
    class Program
    {
        private static Logger logger = LogManager.GetCurrentClassLogger();

        static void Main(string[] args)
        {
            logger.Trace("Sample trace message");
            logger.Debug("Sample debug message");
            logger.Info("Sample informational message");
            logger.Warn("Sample warning message");
            logger.Error("Sample error message");
            logger.Fatal("Sample fatal error message");

            Console.ReadKey(true);
        }
    }
}

コンフィグ

NLog.configファイルにログ出力先としてファイルとコンソールを指定して、それぞれにログレベルを設定します。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">

  <targets>
    <target xsi:type="File" name="logfile" fileName="file.txt"
            layout="${date:format=yyyyMMddHHmmss} ${message}" />
    <target xsi:type="ColoredConsole" name="console" 
            layout="${longdate} ${uppercase:${level}} ${message}" />
  </targets>

  <rules>
    <logger name="*" minlevel="Info" writeTo="logfile" />
    <logger name="*" minlevel="Trace" writeTo="console" />
  </rules>
</nlog>

実行結果

ファイルは直下にInfoログ以上が出力されました。
layout="${date:format=yyyyMMddHHmmss} ${message}"と指定したフォーマットで日付も出力されてます。
f:id:sh_yoshida:20170317165648p:plain

ColoredConsoleを指定したので、コンソールには色付きでTraceログ以上が出力されました。出力内容もコンソール用の内容layout="${longdate} ${uppercase:${level}} ${message}"になってますね。
f:id:sh_yoshida:20170317165455p:plain


Gmailに出力

Gmailにも遅れるようなのでやってみます。

NLog.config

targetsにGmail用の設定を追記します。メールアドレスは適宜読み替えで。パスワードはGmailの二段階認証でアプリパスワードを発行しました。
このとき二段階認証にしてたので別のアプリパスワードを発行。
sh-yoshida.hatenablog.com

<target xsi:type="Mail" name="gmail"
        smtpServer="smtp.gmail.com"
        smtpPort="587"
        smtpAuthentication="Basic"
        smtpUserName="user@gmail.com"
        smtpPassword="password"
        enableSsl="true"
        from="user@gmail.com"
        to="user@gmail.com"
        cc="user@gmail.com"
        />

rulesにはログレベルにFatalを指定して追記しました。

<logger name="*" minlevel="Fatal" writeTo="gmail" />

実行結果

Fatalレベルのログだけメールで飛んできました。簡単♪

f:id:sh_yoshida:20170317172302p:plain

今回はこれくらいで。あとは実際に使うかもってなったらもう少し詳しく踏み込んで調べてみよう。