Кирпичики для ASP .NET MVC 4 (CheckBoxListFor и свой генератор View)

4 Май
2012

Общая задача: Административный Controller к стандартному MembershipProvider.


Зачем:

К сделанным сайтам нужно дать управление пользователями администратору. Стандартного, простого и на русском не нашел.


Для решения созданы несколько кирпичиков и их размещаю в статье.


Кирпичик №1. Индивидуальные шаблоны генератора View (например на русском)

Зачем: Переводить надоело или нужен свой шаблон


Шаблоны лежат тут: «X:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ItemTemplates\CSharp\Web\MVC 4\CodeTemplates\AddView\CSHTML\»

Просто копируем нужный шаблон (например Edit.tt в Edit_Ru.tt), корректируем в нем код на нужный и при генерации View выбираем этот
шаблон.

Кирпичик №3,4,5. CheckBoxFor, генерация CheckBoxFor в View, и сортировка полей модели

Зачем: Встроенного нет.


1. Код CheckBoxListFor найден на просторах сети и немного подправлен



		    public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(
		        this HtmlHelper<TModel> htmlHelper, 
		        Expression<Func<TModel, TProperty[]>> expression, 
		        MultiSelectList multiSelectList, 
		        object htmlAttributes = null)
		    {
		        MemberExpression body = expression.Body as MemberExpression;
		        string propertyName = body.Member.Name;

		        TProperty[] list = expression.Compile().Invoke(htmlHelper.ViewData.Model);

		        List<string> selectedValues = new List<string>();

		        if (list != null)
		        {
		            selectedValues = new List<TProperty>(list).ConvertAll<string>(delegate(TProperty i) 
		               { return i.ToString(); });
		        }

		        TagBuilder divTag = new TagBuilder("div class=\"CheckBoxListFor\"");
		        divTag.MergeAttributes(new RouteValueDictionary(htmlAttributes), true);

		        foreach (SelectListItem item in multiSelectList)
		        {
		            divTag.InnerHtml += String.Format("<label><input type=\"checkbox\" name=\"{0}\" id=\"{0}_{1}\" " +
		                                                "value=\"{1}\" {2} />{3}</label>",
		                                                propertyName,
		                                                item.Value,
		                                                selectedValues.Contains(item.Value) ? "checked=\"checked\"" : "",
		                                                item.Text);
		        }

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


2. Attributes



		/// <summary>
		/// Атрибут для генерации View
		/// </summary>
		public class CheckBoxListAttribute : Attribute
		{
		    /// <summary>
		    /// Путь получения всех значений
		    /// </summary>
		    public string AllValueLink { get; set; }

		    public CheckBoxListAttribute(string allValueLink)
		    {
		        AllValueLink = allValueLink;
		    }
		}

		/// <summary>
		/// Атрибут для сортировкаи полей модели
		/// </summary>
		public class PropertySortAttribute : Attribute
		{
		    /// <summary>
		    /// Порядок сортировки
		    /// </summary>
		    public int SortOrder { get; set; }

		    public PropertySortAttribute(int sortOrder)
		    {
		        SortOrder = sortOrder;
		    }
		}
		


3. Model



		    public class AdvAccountModel
		    {
		        [Required]
		        [DataType(DataType.Password)]
		        [Display(Name = "Текущий пароль")]
		        [PropertySort(-1)]
		        public string OldPassword { get; set; }

		        [Required]
		        [Display(Name = "Имя пользователя")]
		        [PropertySort(0)]
		        public string UserName { get; set; }

		        [Required]
		        [DataType(DataType.EmailAddress)]
		        [Display(Name = "Email")]
		        [PropertySort(1)]
		        public string Email { get; set; }

		        [StringLength(100, ErrorMessage = "{0} должен быть минимум {2} символов.", MinimumLength = 6)]
		        [DataType(DataType.Password)]
		        [Display(Name = "Пароль")]
		        [PropertySort(2)]
		        public string Password { get; set; }

		        [DataType(DataType.Password)]
		        [Display(Name = "Подтверждение пароля")]
		        [Compare("Password", ErrorMessage = "Новый пароль и подтверждение пароля - не совпадают.")]
		        [PropertySort(3)]
		        public string ConfirmPassword { get; set; }

		        [Display(Name = "Секретный вопрос")]
		        [PropertySort(4)]
		        public string PasswordQuestion { get; set; }

		        [Display(Name = "Ответ на секретный вопрос")]
		        [PropertySort(5)]
		        public string PasswordAnswer { get; set; }
		    }

		    public class AdvProfileModel : AdvAccountModel
		    {
		        /// <summary>
		        /// Пустой конструктор
		        /// </summary>
		        public AdvProfileModel()
		        {
		        }

		        /// <summary>
		        /// Конструктор с инициализацие по пользователю
		        /// </summary>
		        /// <param name="user"></param>
		        public AdvProfileModel(string user)
		        {
		            UserName = user;
		            MembershipUser mUser = Membership.GetUser(user);
		            if (user != null)
		            {
		                Email = mUser.Email;
		                PasswordQuestion = mUser.PasswordQuestion;
		            }
		        }

		        [Required(AllowEmptyStrings = true)]
		        [DataType(DataType.Password)]
		        [Display(Name = "Текущий пароль")]
		        [PropertySort(-1)]
		        new public string OldPassword { get; set; }

		        #region Roles

		        string[] _userRoles;
		        [Display(Name = "Роли")]
		        [CheckBoxList("Model.AllRoles")]
		        [ScaffoldColumn(true)]
		        [PropertySort(11)]
		        public string[] UserRoles
		        { 
		            get
		            {
		                if (String.IsNullOrEmpty(UserName))
		                    return new string[0];

		                return Roles.GetRolesForUser(UserName);
		            }
		            set
		            {
		                _userRoles = value;
		            }
		        }

		        /// <summary>
		        /// Все роли
		        /// </summary>
		        public MultiSelectList AllRoles
		        {
		            get
		            {
		                string[] all = Roles.GetAllRoles();
		                List<SelectListItem> ret = new List<SelectListItem>();
		                foreach(string role in all)
		                    ret.Add(new SelectListItem { Selected = false, Text = role, Value = role});

		                MultiSelectList r = new MultiSelectList(ret, "Text", "Value");
		                foreach (SelectListItem item in r)
		                {
		                }
		                return r;
		            }
		        }

		        /// <summary>
		        /// Сохранить роли в БД
		        /// </summary>
		        public void SaveRoles()
		        {
		            if (String.IsNullOrEmpty(UserName))
		                return;

		            string[] all = Roles.GetAllRoles();
		            foreach (string role in all)
		            {
		                var item = Array.Find(_userRoles, x => x == role);
		                if ((String.IsNullOrEmpty(item)) && (Roles.IsUserInRole(UserName, role)))
		                    Roles.RemoveUserFromRole(UserName, role);
		            }

		            foreach (string role in _userRoles)
		                if (!Roles.IsUserInRole(UserName, role))
		                    Roles.AddUserToRole(UserName, role);
		        }

		        #endregion Roles
		    }
		


4. Код для страницы .chtml


		@Html.CheckBoxListFor(model => model.UserRoles, Model.AllRoles)
		


5. Наш шаблон Edit_Ru.tt


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

1) В районе 84 строки. Определяем есть у свойства модели атрибут «CheckBoxListAttribute» и узнаем путь к списку для CheckBoxList.

		foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) {
		    if (property.Scaffold) {
			//AS 2012-05-02
			string IsCheckbox = "";
			foreach(object attr in property.Attributes)
				if (attr.GetType().Name == "CheckBoxListAttribute")
					foreach(PropertyInfo pi in attr.GetType().GetProperties())
						if (pi.Name == "AllValueLink")
							IsCheckbox =  (string)pi.GetValue(attr, null);
			//AS 2012-05-02
		


2) В районе строки 114. Если поле с атрибутом «CheckBoxListAttribute» то шаблон генерируем по-своему.

		        <div class="editor-field">
		<#
		            if (property.IsForeignKey) {
		#>
		            @Html.DropDownList("<#= property.Name #>", String.Empty)
		<#
		//AS 2012-05-02
		            } else if (IsCheckbox != "") {
		#>
		            @Html.CheckBoxListFor(model => model.<#= property.Name #>, <#= IsCheckbox #>)
		<#
		//AS 2012-05-02
		            } else {
		#>
		            @Html.EditorFor(model => model.<#= property.Name #>)
		<#
		            }
		#>
		            @Html.ValidationMessageFor(model => model.<#= property.Name #>)
		        </div>
		


3) В районе строки 163. Изменим модель свойств, через который работает шаблон. К сожалению, я не понял, зачем в таком небольшом коде добавили эту прослойку.

		class ModelProperty {
		    public string Name { get; set; }
		    public string AssociationName { get; set; }
		    public string ValueExpression { get; set; }
		    public string ModelValueExpression { get; set; }
		    public string ItemValueExpression { get; set; }
		    public Type UnderlyingType { get; set; }
		    public bool IsPrimaryKey { get; set; }
		    public bool IsForeignKey { get; set; }
		    public bool IsReadOnly { get; set; }
		    public bool Scaffold { get; set; }
		    //AS 2012-05-02
		    //Attributes
		    public object[] Attributes { get; set; }
		    //Sort Order
		    public int Sort { get; set; }
		    //AS 2012-05-02
		}	
		


4) В районе строки 284. Меняем метод GetEligibleProperties.

		List<ModelProperty> GetEligibleProperties(Type type) {
		    List<ModelProperty> results = new List<ModelProperty>();

		    foreach (PropertyInfo prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) {
		        Type underlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
		        if (prop.GetGetMethod() != null && prop.GetIndexParameters().Length == 0 && IsBindableType(underlyingType)) {
		            string valueExpression = GetValueExpressionSuffix(prop);

		            results.Add(new ModelProperty {
		                Name = prop.Name,
		                AssociationName = GetAssociationName(prop),
		                ValueExpression = valueExpression,
		                ModelValueExpression = "Model." + valueExpression,
		                ItemValueExpression = "item." + valueExpression,
		                UnderlyingType = underlyingType,
		                IsPrimaryKey = IsPrimaryKey(prop),
		                IsForeignKey = IsForeignKey(prop),
		                IsReadOnly = prop.GetSetMethod() == null,
		                Scaffold = Scaffold(prop)
		                //AS 2012-05-02
		                , Attributes = prop.GetCustomAttributes(false)
		                , Sort = Sort(prop)
		                //AS 2012-05-02	
		            });
		        }
		    }

		    //AS 2012-05-02 
		    //Сортировка
		    var result = results.OrderBy(x => x.Sort).ToList();
		    //AS 2012-05-02
		    return result;
		}
		


5) Добавить куда удобно я добавил после метода GetEligibleProperties. Метод возвращает порядок сортировок для свойств модели.

		//AS 2012-05-02
		int Sort(PropertyInfo propertyInfo) {
			object[] attrs = propertyInfo.GetCustomAttributes(true);
			foreach(object attr in attrs)
				if (attr.GetType().Name == "PropertySortAttribute")
					foreach(PropertyInfo pi in attr.GetType().GetProperties())
						if (pi.Name == "SortOrder")
							return (int)pi.GetValue(attr, null);

			return 0;
		}
		//AS 2012-05-02
		


6) В конце файла. Чтобы все свойства попали в список на генерацию.

		bool IsBindableType(Type type) {
		   //AS 2012-05-02
		   return true;
		   //return type.IsPrimitive || bindableNonPrimitiveTypes.Contains(type);
		   //AS 2012-05-02
		}
		


Итого:

В результате наш шаблон будет правильно сортировать поля при генерации View, и будет правильно создавать код для «CheckBoxListFor»

На этом первый пост заканчиваю. Инвайта нет — отвечать не смогу. Если пост будет интересен, то оформлю еще пост с кодом (выдержками из кода в пост и исходный код отдельно) для решения основной задачи – «Административный Controller к стандартному MembershipProvider».
По материалам Хабрахабр.



загрузка...

Комментарии:

Наверх