Недавно наткнулся на очень забавный и неочевидный баг студии. Суть бага – студия начинает подвисать при сохранении aspx/ascx файлов секунд так на 20-30. Через полминуты все проходит, и можно работать дальше. Воспроизводится стабильно, простым нажатием Ctrl+S. При этом студия не подвисает намертво, просто не отзывается на действия, и вот значок сохранения не пропадает из статус бара:

CropperCapture[2]

Запускаем второй экземпляр студии. Снимаем в DebugOptions and Settings чекбокс Enable Just My Code. Цепляемся им к первому экземпляру студии:

Options

attach

Нажимаем Ctrl+S в проблемном экземпляре студии. Видим в Output кучу исключений:

NotFound

Включаем в DebugExceptions остановку при бросании System.IO.FileNotFoundException. Сохраняем еще раз, смотрим на детали исключения:

breakRegion

В проекте нет явных ссылок на antlr.runtime. Но есть ссылка на DDay.iCal, которая хоть и ссылается на antlr, но не требует ее для нормальной работы. Распространяется DDay, естественно, без проблемной сборки.

При сохранении aspx-файла студия пытается пересоздать файл aspx.designer.cs. Для этого ей нужно получить типы всех элементов управления, упоминаемых в разметке. Например, студия видит в разметке элемент вроде:

Region2

Для получения конкретного типа PostBackTriggerControl студия берет список нейспейсов с префиксом lsf из конфигурации, и пытается найти каждую комбинацию “namespace.type”. А неймпейсов с префиксом lsf у нас оказалось много:

config

Студия достаточно быстро находит нужный ей LogicSoftware.EasyProjects.Web.Gui.Code.PostBackTriggerControl в сборке LogicSoftware.EasyProjects.Web.Gui.Code. Но на этом она не останавливается.

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

Бешенное количество попыток – количество элементов на странице * количество регистраций неймспейсов * количество сборок на которые ссылается сайт * количество проблемных сборок -  и дает те самые 210 исключений при одном нажатии Ctrl+S. Исправляется подбрасываеним недостающих сборок в папку с зависимостями. Добавлять ссылки из web-проекта на них или копировать bin – необязательно. Достаточно, чтобы студия увидела их при поиске.

Недавно напоролись на один древний баг в ASP.NET Membership. Краткий смысл бага – бросание исключения при инициализации провайдера Membership уводит весь Membership в нерабочее состояние.

Полный смысл бага:

Вот код статического метода System.Web.Security.Membership.Initialize(), который вызывается при любых обращениях к методам ProviderBase:

private static void Initialize()
{
    if (!s_Initialized || !s_InitializedDefaultProvider)
    {
        if (s_InitializeException != null)
        {
            throw s_InitializeException;
        }
        if (HostingEnvironment.IsHosted)
        {
            HttpRuntime.CheckAspNetHostingPermission(AspNetHostingPermissionLevel.Low, "Feature_not_supported_at_this_level");
        }
        lock (s_lock)
        {
            if (!s_Initialized || !s_InitializedDefaultProvider)
            {
                if (s_InitializeException != null)
                {
                    throw s_InitializeException;
                }
                bool initializeGeneralSettings = !s_Initialized;
                bool initializeDefaultProvider = !s_InitializedDefaultProvider
                    && (!HostingEnvironment.IsHosted || (BuildManager.PreStartInitStage == PreStartInitStage.AfterPreStartInit));
                if (initializeDefaultProvider || initializeGeneralSettings)
                {
                    bool flag3;
                    bool flag4 = false;
                    try
                    {
                        RuntimeConfig appConfig = RuntimeConfig.GetAppConfig();
                        MembershipSection membership = appConfig.Membership;
                        flag3 = InitializeSettings(initializeGeneralSettings, appConfig, membership);
                        flag4 = InitializeDefaultProvider(initializeDefaultProvider, membership);
                    }
                    catch (Exception exception)
                    {
                        s_InitializeException = exception;
                        throw;
                    }
                    if (flag3)
                    {
                        s_Initialized = true;
                    }
                    if (flag4)
                    {
                        s_InitializedDefaultProvider = true;
                    }
                }
            }
        }
    }
}

Во втором ASP.NET баг осложняется еще и тем, что стандартная реализация ProvidersHelper.InstantiateProvider проглатывает оригинальные исключения и оборачивает их в ConfigurationErrorsException, сохраняя лишь текст сообщения

Не используйте Membership. А если используете – не бросайте исключения при инициализации своих провадеров.

Пару лет назад наш основной продукт, EasyProjects .NET 6.x, было решено оставить на вялом саппорте команды из 4 человек. И написать на замену новый, свежий, EasyProjects .NET 7 AikiProjects BirdView Projects, с дамами, преферансом, и кучей JavaScript-а. Разработка BirdView, естественно, затянулась. EasyProjects .NET же плавно дописался до версии 7.1, оброс Enterprise-фичами, продался на семизначную сумму за год. Появились толстые клиенты, покупающие по лицензии на 200-300 пользователей. При том, что в начале разработки мы считали что 50 пользователей и 100 проектов – это очень много, и “будет тормозить”.

С моей колокольни (отдела custom development) было интересно наблюдать, как EP.NET все меньше подходил для управления Software Development Projects. И все больше адаптировался под управление не-IT проектами. Я только догадываюсь, как наш софт использует DNA Lab в FBI, или Enterprise Serviсes в Microsoft. Но они явно не сторонники гибких методологий.

И вот месяц назад концепция поменялась. EasyProjects .NET больше не умирает. Его ждет бурное развитие, 80% рост продаж (если Sales не врет), и еще более Enterprise-плюшки:

  • Resource Management
  • Multiple Dependencies
  • Financial Reporting module
  • Critical Path
  • Enterprise Edition (куда ж без него)
  • и много-много других мелких изменений

Смена концепции внезапно забросила меня на роль Product Owner-а. Прокси, естественно. Пытаюсь достичь нирваны общего вижина понять, во что должен превратиться софт хотя бы в течении полугода. Пока понимание очень смутное, но оптимистичное.

Строили, строили, и наконец построили. Новый проект, который должен будет кормить фирму следующие N лет, дорос до беты.

image

Отдам ключи от беты в хорошие руки :)

Первая часть, где мы остановились на рабочих выпадающих списках с нерабочей валидацией.

Для того, чтобы стандартная клиентская валидация заработала, нужно сделать следующее:

  1. Заполнить метаданные. Мы сделали это в прошлой части.
  2. Заполнить поля ViewData.TemplateInfo. В нашем случае – из TemplateInfo родительского действия.
  3. Отрендерить выпадающий список на основе метаданных и данных шаблона.

Изменения затронут только представление выпадающего списка.

ViewsListDropDown.cshtml

@model SelectList
@{
   
this
.ViewContext.ViewData.TemplateInfo.FormattedModelValue
        =
this
.ViewContext.ParentActionViewContext.ViewData.TemplateInfo.FormattedModelValue;
       
   
this
.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix
        =
this
.ViewContext.ParentActionViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
}
@
Html.DropDownList(
   
""
,
   
this
.Model,
   
this.ViewData.ModelMetadata.NullDisplayText
    )

Важно: ни в коем случае нельзя подменять ссылку ViewData.TemplateInfo на объект из родительского контекста. ASP.NET MVC рендерит атрибуты валидации только для первого элемента управления с заданным именем. В нашем примере это приведет к отсутствию валидаторов у всех списков, кроме первого.

После этого валидаторы заработают, и даже слишком хорошо:

image

В стандартном валидаторе целочисленного формата есть баг – он учитывает только атрибут value у элементов списка, и не учитывает innerText. Т.е. выбранный на картинке выше <option>1992</option> он числом не считает.

Для обхода этого бага можно создать SelectList с явным указанием имен свойств для Text/Value:

ControllersListController.cs

public ActionResult Years()
{
   
int? selectedYear = this.ControllerContext.ParentActionViewContext.ViewData.Model as int
?;

   
var model = new SelectList(Enumerable.Range(1990, 20).Select(y => new
{ Value = y }),
       
"Value", "Value"
, selectedYear);

   
this
.ViewData.Model = model;
   
this.ViewData.ModelMetadata = this
.ControllerContext.ParentActionViewContext.ViewData.ModelMetadata;

   
return View("DropDown");
}

Некрасиво, но работает. Надеюсь, баг исправят в релизе :)

Исходный код к этой статье: MvcDropDowns2.zip. Требует ASP.NET MVC 3.0 RC2 для запуска.