ASP.NET MVC: Сохраняем настройки сайта в свою секцию файла конфигурации web.config

ru-RU | created at: 9/22/2012 | published: 9/22/2012 | updated at: 1/2/2018 | number of views: 8095

Мне много раз приходилось сохранять настройки сайта в файле конфигурации. Раздел appSettings предоставляет возможность хранить настройки по принципу "ключ" = "значение" (Dictionary). Я же хочу показать как можно создать свою секцию в файле конфигурации, как сохранять новые и обновленные значения.

Что будет в статье?

В предыдущей части были созданы сами настройки и реализовано чтение настроек. В этой предстоит создать форму управления настройками, ajax-сервис, ViewModel на javascript и всё что ещё потребуется.

Сохранение настроек в web.config

Для начал доработаем немного Config-помощник, дописав в него один новый метод, которой будет сохранять настройки:

internal static Configuring Save(SiteSettings settings) {
    Configuring success = new Configuring();
    try {
        Configuration cfg = WebConfigurationManager.OpenWebConfiguration("~");
        var group = cfg.SectionGroups[CONFIGGROUPNAME];
        SiteSettings section = (SiteSettings)group.Sections[CONFIGSECTIONNAME];
        if (section != null) {
            section.Lenta.AllowPostFromShare = settings.Lenta.AllowPostFromShare;
            section.Lenta.DeleteAfterDays = settings.Lenta.DeleteAfterDays;
            section.PagerSize.Clear();
            for (int i = 0; i < settings.PagerSize.Count; i++) {
                section.PagerSize.Add(settings.PagerSize[i]);
            }
            cfg.Save();
            success.Success = true;
        }
    }
    catch (ConfigurationErrorsException error) {
        success.ConfigException = error;
    }
    return success;
}

Рисуем форму редактирования

В панели управления администратора я сделал ссылку на представление Settings.cshtml. И вот так, не сложно, наполнил это представление html-разметкой:

<div class="clear">
    <div class="left" style="width: 49%">
        <h4>Лента</h4>
        <div class="editor-label">
            <label for="DeleteAfterDays">Очищать старее чем, дни:</label>
        </div>
        <div class="editor-field">
            <input type="text" id="DeleteAfterDays" value="" />
        </div>
        <div class="editor-label">
            <input type="checkbox" id="AllowPostFromShare" />
            Разрешить публикацию через ускоритель
        </div>
    </div>
    <div class="left" style="width: 49%">
        <h4>Пользовательский интерфейс</h4>
        <h6>Настройки пейджера для сущностей</h6>
        <div></div>
        <button>+</button>
    </div>
</div>

<div class="clear"></div>
<p>
    <button>Сохранить все</button>
</p>

И раз уж я решил реализовать задуманное с использованием фреймворка Knockout.js, то далее это представление наполнится атрибутами привязки (data-bind). А пока надо подключить требуемые скрипты к этому представлению. Хочу заметить, что это пока не полный список.

@section scripts{
    <script src="@Url.Content("~/scripts/knockout-2.1.0.js")"></script>
    <script src="@Url.Content("~/scripts/knockout.validation.js")"></script>
}

Лирическое отступление

Я набросал небольшой Nuget-пакет, который при установки создает папку Js и помещает туда пока один единственный файл site.core.js (читателям моего блога название должно быть знакомо). Далее я постараюсь наполнить контролами и различными полезными штуками этот пакет, по мере написания сайтов. Это хороший старт для вашего фреймворка на JavaScript для всего сайта. Файл содержит обертку на jQuery методы getJSON и postJSON. Я установил себе в проект этот пакет:

PM> Install-Package JsSite
Successfully installed 'JsSite 0.1.0'.
Successfully added 'JsSite 0.1.0' to Calabonga.Mvc.Humor.

PM>

Пакеты и скрипты установлены. Давай подготовим серверную часть.

Создаем новый контролер

Название для контролера AjaxController говорит само за себя. Этот контролер будет обслуживать весь сайт. Но пока в нем будет только пара методов. Вот первый, он читает данные из конфигурации:

public JsonResult LoadSettings() {
    SiteSettings config = Config.Get();
    var jsonConfig = new SiteSettingsJson();
    jsonConfig.Lenta.AllowPostFromShare = config.Lenta.AllowPostFromShare;
    jsonConfig.Lenta.DeleteAfterDays = config.Lenta.DeleteAfterDays;
    for (int i = 0; i < config.PagerSize.Count; i++) {
        jsonConfig.PagerSize.Add(new PagerSizeItem {
            Name = config.PagerSize[i].Name,
            Size = config.PagerSize[i].Size
        });
    }
    return Json(jsonConfig, JsonRequestBehavior.AllowGet);
}

Следует остановиться на минутку и описать казус, который у меня приключился. В строке 2 я получаю объект и при попытке его отправить через JsonResult в строке 12 выдавалась ошибка сериализации объекта SiteSettings. Сам по себе объект простой, но его предки (вспомните от чего он унаследован) упорно не хотели проходить сериализацию. Пришлось сделать простой прокси-класс SiteSettingsJson, и, наполняя его данными передавать на форму. Класс настолько прост, что я даже его приводить не буду, да?

Время для Js-сервиса

Новый файл в папке Js я назвал site.services.js. Вот его содержимое:

/// <reference path="site.core.js" />
/// <reference path="../Scripts/jquery-1.8.1.js" />
/// <reference path="../Scripts/knockout-2.1.0.debug.js" />

(function (site) {

    "use strict";

    site.services.settings = {
        load: function (callback) {
            site.fw.ajaxService.getJson("LoadSettings", {}, callback);
        },
        save: function (jsonData, callback) {
            site.fw.ajaxService.postJson("SaveSettings", jsonData, callback);
        }
    }

})(site);

Этот сервис использует обертку на ajax, которую я получил вместе с JsSite-пакетом. Как вы видите есть уже и второй метод, который будет сохранять данные, но его я пока не писал в AjaxController’e, займусь им позже.

Основной ViewModel на JavaScript? Легко!

Для начала нужно прочитать настройки и отобразить их при открытии страницы. Для этого я создал ViewModel страницы Settings.cshtml, который “умеет” загружать настройки. Файл я назвал site.vm.settings.js положил его в папку Js:

/// <reference path="site.core.js" />
/// <reference path="site.services.js" />
/// <reference path="../Scripts/jquery-1.8.1.js" />
/// <reference path="../Scripts/knockout.mapping-latest.debug.js" />
/// <reference path="../Scripts/knockout-2.1.0.debug.js" />

(function (site) {

    "use strict";

    site.vm.settings = function () {
        var
            //статус работы сервиса
            isbusy = ko.observable(false),

            // конфигурация
            config = ko.observable({
                "Lenta": ko.observable({
                    "DeleteAfterDays": ko.observable(),
                    "AllowPostFromShare": ko.observable(true)
                }),
                "PagerSize": ko.observableArray()
            }),

            // PagerSize: название 
            newName = ko.observable("Entity"),

            // PagerSize: размер страниц
            newSize = ko.observable(0),

            // метод загрузки конфигурации
            load = function () {
                isbusy(true);
                site.services.settings.load(function (json) {
                    isbusy(false);
                    ko.mapping.fromJS(json.Config, {}, config);
                });
            },

            // сохранение настроек
            save = function () {
                isbusy(true);
                var jsonData = ko.toJSON(config);
                site.services.settings.save(jsonData, function (json) {
                    isbusy(false);
                    alert(json)
                })
            },

            // добавдение объекта в список PagerSize
            add = function () {
                config().PagerSize.push(new PagerSize(newName(), newSize()));
                newName("Entity"); newSize(10);
            },

            // удаление объекта из списка PagerSize
            remove = function (item) {
                config().PagerSize.remove(item);
            };

        load();

        return {
            config: config,
            isbusy: isbusy,
            remove: remove,
            add: add,
            newName: newName,
            newSize: newSize,
            save:save
        }
    }();

})(site);

Не думаю, что нужно подробно останавливаться на распечатке. Тем более, что я постарался с комментариями.

Представление Settings.cshtml

Теперь надо показать html-код разметки этого самого представления Settings.cshtml, потому что я кое-что добавил и “нашпиговал” атрибутами привязки. Приведу этот код тоже целиком:

@{
    ViewBag.Title = "Настройки системы";
    Layout = "~/Views/Shared/_LayoutMain.cshtml";
}

<h2>Настройки системы</h2>
<div data-bind="ifnot: isbusy">
    <div class="clear">
        <div class="left" style="width: 49%">
            <h4>Лента</h4>
            <div class="editor-label">
                <label for="DeleteAfterDays">Очищать старее чем, дни:</label>
            </div>
            <div class="editor-field">
                <input type="text" id="DeleteAfterDays"
                     data-bind="value: config().Lenta().DeleteAfterDays" />
            </div>
            <div class="editor-label">
                <input type="checkbox" id="AllowPostFromShare"
                     data-bind="checked: config().Lenta().AllowPostFromShare" />
                Разрешить публикацию через ускоритель
            </div>
        </div>
        <div class="left" style="width: 49%">
            <h4>Пользовательский интерфейс</h4>
            <h6>Настройки пейджера для сущностей</h6>
            <div data-bind="foreach: config().PagerSize">
                <p>
                    <b><span data-bind="text: Name"></span></b>&nbsp;на одной странице <b>
                        <span data-bind="text: Size"></span></b>
                    <button data-bind="click: $parent.remove">х</button>
                </p>
            </div>
            Название класса сущности:<br />
            <input type="text" data-bind="value: newName" /><br />
            Количество объектов на странице:<br />
            <input type="text" data-bind="value: newSize" /><br />
            <button data-bind="click: add">Добавить новую</button>
        </div>
    </div>

    <div class="clear"></div>
    <p>
        <button data-bind="click: save">Сохранить все</button>
    </p>
</div>
@section scripts{
    <script src="@Url.Content("~/scripts/knockout-2.1.0.js")"></script>
    <script src="@Url.Content("~/scripts/knockout.validation.js")"></script>
    <script src="@Url.Content("~/scripts/knockout.mapping-latest.js")"></script>
    <script src="@Url.Content("~/js/site.core.js")"></script>
    <script src="@Url.Content("~/js/site.services.js")"></script>
    <script src="@Url.Content("~/js/site.vm.settings.js")"></script>
    <script>
        $(function () {
            ko.applyBindings(site.vm.settings);
        });
    </script>
}

Ну, и как это выглядит, чтобы уж совсем всё было наглядно:

Слева я значение 17 поменяю на 15, а  правом списке к уже существующим: Logs, Exhibit, Lenta добавил еще одну сущность Comment и теперь в режиме отладки хочу проверить, приходят ли данный в AjaxController. А-а-а-а-а вот они-и!

96-savesettings

Осталось только “прикрутить” валидацию", но это пусть будет уже вашим домашним заданием, тем более, что об этом уже был разговор. А еще надо написать метод сохранения данных. Так как у меня появился прокси-класс для настроек, то теперь я могу использовать его как входящие данные для метода сохранения в Config-помощнике. Итак, представлю второй метод Config-помощника:

internal static Configuring Save(SiteSettingsJson settings)
{
    Configuring success = new Configuring();
    try
    {
        Configuration cfg = WebConfigurationManager.OpenWebConfiguration("~");
        var group = cfg.SectionGroups[CONFIGGROUPNAME];
        SiteSettings section = (SiteSettings)group.Sections[CONFIGSECTIONNAME];
        if (section != null)
        {
            section.Lenta.AllowPostFromShare = settings.Lenta.AllowPostFromShare;
            section.Lenta.DeleteAfterDays = settings.Lenta.DeleteAfterDays;
            section.PagerSize.Clear();
            for (int i = 0; i < settings.PagerSize.Count; i++)
            {
                section.PagerSize.Add(new PageSizeItemsElement()
                {
                    Name = settings.PagerSize[i].Name,
                    Size = settings.PagerSize[i].Size
                });
            }
            cfg.Save();
            success.Success = true;
        }
    }
    catch (ConfigurationErrorsException error)
    {
        success.ConfigException = error;
    }
    return success;
}

Вот еще маленький класс упомянутый в предыдущем листинге, для полноты картины:

public class Configuring
{
    public bool Success { get; set; }

    public ConfigurationErrorsException ConfigException { get; set; }
}

И, наконец, завершим начатое. Вот конечный вариант второго метода AjaxController’а, который сохраняет данные:

[HttpPost]
public JsonResult SaveSettings(SiteSettingsJson config) {
    Response.CacheControl = "no-cache";

    string message=string.Empty;

    if (ModelState.IsValid)
    {
        Config.Save(config);
        message = "Настройки успешно сохранены";
    }
    else { message = "Неверные данные в конфигурации"; }
    return Json(message);
}

Заключение

Данные читаются, изменяются, сохраняются – значит поставленная цель достигнута и моя миссия завершена. Вас же я прошу писать комментарии.

Да прибудет с вами сила!