ASP.NET MVC: Делаем голосование на сайте при помощи Knockout

ru-RU | создано: 15.01.2013 | опубликовано: 15.01.2013 | обновлено: 31.12.2017 | просмотров за всё время: 9409

В предыдущей статье экспериментировали с формой обратной связи. Магия Knockout позволила максимально быстро и просто реализовать функционал формы обратной связи. На этот раз попробуем что-нибудь поинтереснее, например, "голосование".

В качестве вступления

Я буду строить решение на базе проекта, который был опубликован в предыдущей статье “ASP.NET MVC: И снова про форму обратной связи или куда еще втыкнуть Knockout”. Хочу чтобы статья получилась короткая, но максимально информативная.

Задача на эту статью – “реализовать голосование на сайте, с отображением результатов”.

Рис.1 “Опрос на сайте (пользователь еще не голосовал)”

Рис.1 “Опрос на сайте (пользователь еще не голосовал)”

И второй вариант отображения формы, это когда пользователь уже проголосовал.

106-knockout-poll-1

Рис.2 “Опрос на сайте (пользователь уже голосовал)”

Звучит и выглядит просто, и поэтому я стороной обойду загрузку данных с сервера, запись данных на сервер, чтение настроек и проверку cookies, чтобы по возможности максимально рассказать о Knockout. Итак, если учесть, что все приготовления выполнены, то можно сразу приступить к программированию.

Входные данные

В папке Scripts/App создаю новый файл site.vm.polls.js. Теперь первым делом определю формат данных для объекта (Poll) (листинг 1):

var pollData = {
    "question": "За двумя зайцами погонишься...",
    "answers": [
        { votes: 11, name: "ни одного не поймаешь." },
        { votes: 5, name: "больше двух не поймаешь." },
        { votes: 15, name: "ни одного волка не поймаешь." },
        { votes: 9, name: "порвешься на две части." },
        { votes: 39, name: "зачем бегать, лучше на авто!" }
    ],
    "selectedAnswer": null};

Это, так сказать, входные данные. В моем случае, они “жестко” прописаны в коде. Вы же можете сделать получение данных для опроса с сервера, через Web API, или с сервиса WCF. Немного комментариев относительно представленного кода.

  • Строка 2: Сам вопрос.
  • Строка 3-8: Это предложенные варианты ответа на поставленный вопрос.
  • Строка 10: Ответ, который возможно уже дал пользователь, посещая сайт. Вы можете хранить его в базе данных или даже в cookies. Сейчас он у меня null, поэтому форма должна отобразить список ответов и вопрос. А если поле было заполнено (выбранный ответ), то форма должна отобразить результаты голосования.

Класс Answer (Ответ)

В предыдущем листинге (листинг 1) в строка 3 эти самые ответы на вопрос. “Завернем” класс в JavaScript (листинг 2):

site.vm.Answer = function (value, total) {
       var isSelected = ko.observable(false),
           votes = ko.observable(total),
           name = ko.observable(value);
       return {
           isSelected: isSelected,
           name: name,
           votes: votes
       };
   },

Всё просто, единственное, что хотелось быть отметить, что свойство isSelected будет указыват на то выбран ли ответ в списке вариантов или нет.

Класс Poll (Опрос)

Этот класс и будет представляет “входные данные” (см. листинг 1). Самый большой класс в моем решении (листинг 3).

site.vm.Poll = function (data) {
        var selectedItem = ko.observable(new site.vm.Answer()),
            question = ko.observable(data.question),
            answers = ko.observableArray([]),
            answer = ko.observable(),
            maxVotes = ko.observable(),
            init = function () {
                var max = 0;
                var i;
                for (i in data.answers) {
                    var cur = data.answers[i].votes;
                    if (cur && cur>max) {
                        max = cur;
                    }
                }
                max = Math.round(max * 1.1);
                maxVotes(max);
                for (i in data.answers) {
                    answers.push(new site.vm.Answer(data.answers[i].name, data.answers[i].votes));
                }
                if (data.selectedAnswer) {
                    answer(data.selectedAnswer);
                }
            },
            setSelected = function (item) {
                if (!item.isSelected()) {
                    selectedItem(item.name());
                    ko.utils.arrayForEach(answers(), function (i) {
                        if (i.isSelected && i.isSelected()) { i.isSelected(false); }
                    });
                    item.isSelected(true);
                }
            };

        init();

        return {
            max: maxVotes,
            selectedItem: selectedItem,
            question: question,
            answers: answers,
            answer: answer,
            setSelected: setSelected
        };
    };

И опять немного комментариев:

  • Строка 2: Представляет выбранный пользователем ответ.
  • Строка 6: Вообще-то, это свойство (maxVotes) я ввел только для того, чтобы правильно работал jQuery UI Progressbar, при помощи которого я планирую отображать результаты.
  • Строка 7: Инициализация класса на основании входных данных. Запуск инициализации происходит в строке 35.

ViewModel для формы

На форме может быть много опросов, в моем варианте он один. Создаем ViewModel для формы Index.cshtml. Вот так выглядит pollViewModel (листинг 4):

site.vm.pollViewModel = function () {
        var message = ko.observable("Выберите ваш вариант ответа на вопрос."),
            poll = site.vm.Poll(pollData),
            save = function () {
                poll.answer(poll.selectedItem());
            };

        return {
            save: save,
            poll: poll,
            message: message
        };
    }();

Строка 2: Сообщение для пользователя. Мне не пригодилось, но можно использовать для вывода ошибок при работе сервиса.

Строка 3: Создаем экземпляр класса голосования (см. листинг 3).

Строка 4: Функция сохранения результатов голосования. В моем решении это просто обновления свойства answer. Но вы можете отправить результат на сервер или сохранить в cookies.

Магия Knockout – applyBindings

Голосование (Poll) я планирую показать на главной странице (то есть в котроллере Home, метод – Index).  Я открыл представление (index.cshtml) и полностью его отчистил, и добавил туда такую разметку:

<div data-bind="ifnot: poll.answer">
    <h2 data-bind="text: poll.question">h2>
    <h4 data-bind="text: message">h4>
    <div data-bind="foreach: poll.answers">
        <div data-bind="click: $parent.poll.setSelected" class="big">
            <input type="radio" value="true" 
                      data-bind="checked: isSelected().toString()" class="poll" />
            <span data-bind="text: name, css: {'selected': isSelected}">span>
        div>
    div>
    <p>
        <button data-bind="click: save">голосоватьbutton>
    p>
div>
<div data-bind="if: poll.answer">
    <h2 data-bind="text: poll.question() ' ' poll.answer()">h2>
    <div data-bind="foreach: poll.answers">
            <span data-bind="text: name" >span>
            <div data-bind="progressbar: {value: votes(), max: $parent.poll.max()}">div>
        <br/>
    div>
div>

Всё на форме просто и, надеюсь, понятно. Единственное, что хотелось бы отметить, для отображения результатов в шаблоне (строка 18) используется bindingHandler, который я назвал “progressbar”. Когда вы установите nuget-пакет JsJite, у вас в папке scripts/app появится файл site.bindingHandlers.js,  в котором уже много полезных “штучек”.

Заключение

В качестве заключения хочу предложить демо-проект и поэкспериментировать.