суббота, 29 декабря 2012 г.

Динамическое контекстное меню в ASP.NET MVC



Не так давно у меня появилась необходимость в создать контекстное меню с настраиваемым из code-behind числом элементов для моего ASP.NET MVC приложения. Потраченные полчаса на поиск готового решения не увенчались успехом, в связи чем пришлось заниматься созданием данной функциональности самостоятельно. О том что у меня получилось я хочу рассказать в своей статье.
Итак за основу для решения задачи был взят простенький JQuery-плагин (http://www.trendskitchens.co.nz/jquery/contextmenu/) который преобразует список div с находящимся в нем списком ul в контекстное меню. С динамической генерацией списка проблем нет, более сложным выглядел вопрос динамического создания байндинга обработчиков элементов меню. Но как оказалось это тоже вопрос пяти минут. В итоге получилось простое и достаточно эффективное решение.
Для демонстрации я создал простенькое приложение. При нажатии на зеленую область правой кнопкой мыши появляется меню:


При нажатии на один из элементов меню появляется всплывающее окно с сообщением. В случае представленном ниже мы выбрали меню “Save”.

Итак, пару слов о реализации.

Первым делом подключаем JQuery и плагин контекстного меню:
        <script src="~/Scripts/jquery-1.8.0.min.js" type="text/javascript"></script>
        <script src="~/Scripts/jquery-ui-1.8.23.custom.min.js" type="text/javascript"></script>

        <script src="~/Scripts/jquery.contextmenu.r2.packed.js" type="text/javascript"></script>

В контроллере инициализируем коллекцию элементов меню:
        public ActionResult Test()
        {
            ViewBag.MenuItems = new[] { "Open", "Save", "Edit", "Exit" };
            return View();
        }

В отображении создаем два слоя, первый слой – зеленая область, второй слой – контекстное меню, там генерируем список элементов, которые получили от контроллера:
@{   
    var mi = (IEnumerable<string>)ViewBag.MenuItems;
}

<div id="mainDiv">
    <p>Right click here</p>
</div>

<div class="contextMenu" id="myMenu1">
    <ul>
        @foreach (var item in mi)
        {
            <li id="@item"><img src="@Url.Content("~/Content/images/edit.png")" />@item</li>
        }
    </ul>
</div> 

Добавляем стиль для первого слоя в Site.css:
#mainDiv {
    width: 300px;
    height: 300px;
    background: limegreen;
}

Итак, все что теперь осталось это подключить плагин и настроить байндинг обработчиков. Суть моего решения заключается в том, что мы на лету для каждого элемента меню генерируем соответствующий элемент в байндинге. Обработчиком каждого из элементов будет функция которая просто выводит название элемента меню, который был выбран. Код представленный ниже демонстрирует реализацию.
<script type="text/javascript">

    $(function () {

        $('#mainDiv').contextMenu('myMenu1', {
            @if(mi.Any())
            {
                Html.ViewContext.Writer.WriteLine("bindings: {");
                int count = mi.Count();
                int i = 1;

                foreach(var item in mi)
                {
                    Html.ViewContext.Writer.WriteLine(string.Format("'{0}': function(t) {{ menuHandler('{0}'); }}{1}", item, i == count ? string.Empty : ","));
                    i++;
                }
                Html.ViewContext.Writer.WriteLine("}");
            }
        });
    });
   
    function menuHandler(obj) {
        alert('You can handle menu for "' + obj + '" as you wish');
    }

</script>

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

пятница, 30 ноября 2012 г.

Топ-10 источников ошибок в программном продукте




Нет ни одного программного продукта в котором нету ни единой ошибки. Это аксиома и все про это знают. И если с мелкими и незначительными ошибками, которые не препятствуют использованию продукта можно мирится, то другие ошибки, которые порой вызывают катастрофы мирового масштаба нужно что то делать. Ниже я попытаюсь разобраться в причине, почему же все таки такие ошибки возникают, что является их причиной и можно ли сделать что нибудь что бы их устранить еще на самом раннем этапе. Итак, топ-10 источников ошибок в программном продукте.

1.     Человеческий фактор

Итак это самое очевидное и с этим пунктом все понятно. Любой программист является обыкновенным человеком, которому свойственно делать ошибки, даже самому квалифицированному, и самому опытному. Тут ничего нельзя поделать и нету универсального средства, как избежать таких ошибок. Разве что попросить  разработчиков быть повнимательнее.

2.     Проблемы коммуникации

Источником это проблемы может быть непонимание разработчиком в деталях того, что от него хотят. В итоге получим продукт который выглядит как задумано, но делает все немного не так. Другой причиной ошибок из этой категории может быть  неочевидность и нелогичность разрабатываемой функциональности. Чем более запутанная логика тем больше шансов, что ее реализацию будет неидеальна. Что можно сделать в этом случае? Достаточно будет просто сделать все требования простыми, логичными и понятными. Согласен это не всегда возможно, но нужно хотя бы к этому стремится.

3.     Сжатые сроки разработки

Любая задача, даже самая простая, требует времени на осмысление, проектирование, реализацию и, наконец, тестирование. В реальности часто бывает что программные продукты создаются в сжатые сроки и с недостаточными ресурсами. Как результат времени хватает только на, собственно, реализацию, и то не всегда. Как результат времени на проектирование, тестирование и все остальное просто нету. Результат очевиден. Что-то делать? Ответ лежит на поверхности добавить времени и ресурсов.

4.     Плохое проектирование

Данная проблема может иметь место про разработке сложной большой системы. Процессу проектирования не предшествовал в должной мере этап анализа требований, изучение всевозможных нюансов работы системы и поиск наиболее подходящих решений для реализации. Причиной того, что качество проектирования не было высоким, не обязательно должна быть недостаточная квалификация проектировщиков, чаще причиной является опять таки недостаток времени или внутренние правила проектирования которые препятствовали созданию качественной архитектуры. Решение очевидно –потратить на проектирование столько времени сколько нужно, а не столько, сколько возможно.

5.     Плохие практики разработки

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

6.     Отсутствие системы контроля версий

Казалось бы причем тут система контроля версий. Благодаря ей можно отслеживать все изменения в коде, весь прогресс проделанной работы, иметь перед глазами информацию о найденных и исправленных дефектах и изменениях. Список тест-кейсов для регрессионного тестирования позволит вам никогда не пропустить какую нибудь мелочь, которая приведет к большому краху вашего продукта. Используйте систему контроля версий – она действительно сделать вещи проще и лучще.

7.     Глючные сторонние инструменты

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

8.     Недостаток квалифицированного тестирования

Увы, такое тоже встречается везде и повсеместно. Даже если на вашем проекте уделяется достаточно внимания тестированию и ресурсам для тестирования это не значит что это полностью решит проблему с дефектами на вашем проекте. Постоянно работайте над повышением квалификации ваших QA и совершенствованием применяемых ими методик тестирования.

9.     Изменения за 10 минут до релиза

Я верю что эта проблема становиться все менее актуальной, но понимаю что моей веры недостаточно что бы ликвидировать проблему целиком. Прискорбно, но даже на самом «правильном» проекте случаются ситуации когда вроде бы все и написано и протестировано и в целом все хорошо. Но вот за минуту до того как нужно осуществлять деплой проекта возникает необходимость какого-то незначительного изменения который «ну уж точно ни на что не повлияет» . И очень часто именно это изменение является тем злом которое породило новые дефекты в вашем продукте.

Постойте, а где же десятая причина? Хех, я знаю что она есть, но часто оказывается что отличается от проекта к проекту. Поэтому последний пункт я оставил для вас, моих читателей, что бы вы сами поведали всему миру он вашей десятой причине, прямо здесь в комментариях к этой статье.

Не перевод, но за основу взята статья http://software-testing-zone.blogspot.com/2008/12/why-are-bugsdefects-in-software.html

вторник, 30 октября 2012 г.

Парсер для логических выражений



Не так давно я столкнулся с необходимостью релизации вычисления значения логических выржений представленных в виде обыкновенной строки. Логическое выражение может быть простым, например b > 5 или же составным, и состоять из нескольких груп, как показано ниже.
a <> 0 & (b > 5 | c <> 0) & (d < 5 & e = "test")
В целях формализации  данной задачи, я написал юнит-тест который демонстрирует что конкретно я хочу. Код теста прдставлен ниже.
        [TestMethod]
        public void Demo_Test()
        {
            var statementBody = "a <> 0 & (b > 5 | c <> 0) & (d < 5 & e = \"test\")";
            var dic = new Dictionary<string, string>
                          {
                              { "a", "10" },  { "b", "1" }, { "c", "1" }, { "d", "2" }, { "e", "test" }
                          };

            var checker = new ConditionChecker();
            var result = checker.Check( statementBody , dic);

            Assert.IsTrue(result);
        }
Классический способ решения данной задачи состоит в том, что по выражению формируется дерево, осуществляются вычисления для каждого узла дерева и формируется результат.
Решение которое мне пришло в голову показалось мне  более простым и достаточно эффективным и я решил что это именно то что мне нужно. Суть его заключалась в том, что вместо того что бы по этому выражению стоить дерево, достаточно найти все условные операции в выражении вычислить результат для каждой из них и в выражении вместо условной операции поместить результат. То есть в итоге прийти к следующему выражению которое будет эквивалентным исходному: 
 "1 & (0 | 1) & (1 & 1)"
Здесь 1 – означает что условие выполняется, а 0 –что нет. То есть условие a <> 0 при a = 10 выполняется и поэтому замещаем его единицей, а при b = 1 условие b > 5 не выполняется поэтому замещаем его нулем, И таким же образом поступаем со всеми остальными операциями.
После того как все условия были посчитаны и замещены нулем или единицей, следующим этапом будет вычисления результата логических операций в скобках, то есть сначала вычисляем результат для (0 | 1), потом для (1 & 1), а потом для т ого что получилось 1 & 1 & 1. В итоге получим требуемый результат.
Ниже представлена реализация данного способа в деталях.
        private const char groupStart = '(';
        private const char groupEnd = ')';

        private const char logicalAnd = '&';
        private const char logicalOr = '|';


        private const string operatorEqual = "=";
        private const string operatorGreate = ">";
        private const string operatorLess = "<";
        private const string operatorNotEqual = "<>";


        internal bool Check(string content, Dictionary<string, string> variables)
        {
            var operations = new[] { operatorEqual, operatorNotEqual, operatorGreate, operatorLess };
            var dic = new List<CalculatedCondition>();

            content = groupStart + content + groupEnd;

            foreach (var operation in operations)
            {
                var index = 0;
                do
                {
                    index = content.IndexOf(operation, index);
                    if(index == -1)
                    {
                        break;
                    }

                    if(!dic.Any(c => index > c.BeginIndex && index < c.EndIndex))
                    {
                        int beginIndex = GetExpressionBeginBoundary(content, index, operation.Length);
                        int endIndex = GetExpressionEndBoundary(content, index, operation.Length);

                        var expression = content.Substring(beginIndex, endIndex - beginIndex);
                        var result = this.CheckCondition(expression, variables);

                        dic.Add(new CalculatedCondition { BeginIndex = beginIndex, EndIndex = endIndex, Expression = expression, Result = result });
                    }

                    index += operation.Length;

                } while(true);
            }

            content = dic.Aggregate(content, (current, calculatedCondition) => current.Replace(calculatedCondition.Expression, calculatedCondition.Result ? "1" : "0"));
            content = content.Replace(" ", string.Empty);

            return ComposeResult(content);
        }

Вычисление условия:
        internal bool CheckCondition(string condition, Dictionary<string, string> variables)
        {
            var result = false;

            if (condition.IndexOfAny(new[] { logicalAnd, logicalOr }) != -1)
            {
                // only expressions like "varname > value" are supported
                throw new NotSupportedException();  
            }

            var operations = new [] { operatorEqual, operatorNotEqual, operatorGreate, operatorLess};

            foreach (var operation in operations)
            {
                if(condition.Contains(operation))
                {
                    int index = condition.IndexOf(operation);
                    var verb = condition.Substring(0, index).Trim();
                   
                    var arg = condition.Substring(index + operation.Length).Trim(new [] {' ','\'','\"' });

                    string val = null;
                    if(variables.ContainsKey(verb))
                    {
                       val = variables[verb];
                    }

                    if(val !=null)
                    {
                        decimal num1 = 0, num2 = 0;
                        bool isNum1 = decimal.TryParse(val, out num1);
                        bool isNum2 = decimal.TryParse(arg, out num2);
                       
                        switch (operation)
                        {
                            case operatorEqual:
                                result = isNum1 && isNum2 ? num1 == num2 : val == arg;
                                break;
                            case operatorNotEqual:
                                result = isNum1 && isNum2 ? num1 != num2 : val != arg;
                                break;
                            case operatorGreate:
                                result = !isNum1 || !isNum2 || num1 > num2;
                                break;
                            case operatorLess:
                                result = !isNum1 || !isNum2 || num1 < num2;
                                break;
                        }
                        break;
                    }
                    else
                    {
                        throw new NotSupportedException(
string.Format("Variable {0} doesn't exists in the current context", verb));
                    }
                }
            }
           
            return result;
        }

Подготовка конечного результата:
        private bool ComposeResult(string content)
        {
            // (0|(1&1))&1)

            Debug.WriteLine(content);

            var start = 0;
            var resultChanged = false;

            do
            {
                start = 0;
                resultChanged = false;

                while(true)
                {
                    start = content.IndexOf(groupStart, start);

                    var nextStart = content.IndexOf(groupStart, start + 1);
                    var end = content.IndexOf(groupEnd, start + 1);

                    if (start == -1 && end == -1)
                    {
                        break;
                    } 
                    else
                    {
                        if(end < nextStart || nextStart == -1)
                        {
                            var str = content.Substring(start, (end + 1) - start);
                            bool result = ToBool(str);
                            content = content.Replace(str, result ? "1" : "0");
                            resultChanged = true;
                            break;
                        }

                    }

                    start = nextStart;
                }
               
            }while(resultChanged && start != -1);

            return content == "1";
        }

Вычисление границ выражения:
        internal int GetExpressionBeginBoundary(string content, int operationIndex, int operationLength)
        {
            int resultIndex = -1;
            var index = operationIndex - 1;
            while (content.ElementAt(index) == ' ')
            {
                --index;
            }

            for (; index >= 0; --index)
            {
                var separatorsForNotStrings = new[] { ' ', groupStart, logicalAnd, logicalOr };
                var currentElement = content.ElementAt(index);
                if (groupEnd == currentElement)
                {
                    throw new NotSupportedException(string.Format("Illegal endGroup symbol found at {0}", index));
                }

                if (separatorsForNotStrings.Any(c => c == currentElement))
                {
                    resultIndex = index + 1;
                    break;
                }
            }

            // set begin on expression
            if (resultIndex == -1)
            {
                resultIndex = 0;
            }

            return resultIndex;
        }

        internal int GetExpressionEndBoundary(string content, int operationIndex, int operationLength)
        {
            int resultIndex = -1;
            // skip operation text
            var index = operationIndex + operationLength;
            // skip spaces if present
            while (content.ElementAt(index) == ' ')
            {
                ++index;
            }

            // check if operand is string
            bool isString = false;
            var argumentStartsWith = content.ElementAt(index);
            if(argumentStartsWith == '\'' || argumentStartsWith == '\"')
            {
                isString = true;
                ++index;
            }

            for(; index < content.Length; index++)
            {
                var separatorsForNotStrings = new[] { ' ', groupEnd, logicalAnd, logicalOr };
                var separatorsForStrings = new[] { '\'', '\"' };

                var currentElement = content.ElementAt(index);

                if(groupStart == currentElement)
                {
                    throw new NotSupportedException(string.Format("Illegal startGroup symbol found at {0}", index));
                }

                if(isString)
                {
                    if (separatorsForStrings.Any(c => c == currentElement))
                    {
                        ++index;
                        resultIndex = index;
                        break;
                    }
                }
                else
                {
                    if (separatorsForNotStrings.Any(c => c == currentElement))
                    {
                        resultIndex = index;
                        break;
                    }
                }
            }

            // set end on expression
            if (resultIndex == -1)
            {
                resultIndex = content.Length;
            }

            return resultIndex;

        }
Вспомогательные функции:
        private bool ToBool(string str)
        {
            str = str.Trim(new[] { groupStart, groupEnd });

            var operations = new[] {  logicalAnd, logicalOr };

            if(!operations.Any(o => str.Contains(o)))
            {
                return str == "1";
            }

            var arg1 = str.ElementAt(0) == '1';
            for(int i=1; iLength; i+=2)
            {
                var operation = str.ElementAt(i);
                var arg2 = str.ElementAt(i+1) == '1';

                switch (operation)
                {
                    case logicalAnd:
                        arg1 = arg1 & arg2;
                        break;
                    case logicalOr:
                        arg1 = arg1 | arg2;
                        break;
                }
            }

            return arg1;
        }