Задача.
Необходимо сделать удобную форму добавления файлов на сайт с привязкой к нескольким разделам инфоблока.
На скриншоте показана структура инфоблока, для которого мы создадим форму добавления файлов. Так как стандартный контрол для редактирования секции нас не устраивает, мы сделаем его более удобным. При привязке к секциям мы должны делать проверку на обязательную привязку по категории и языку программирования. Привязку по технологиям мы сделаем множественной и не обязательной. По соображениям безопасности все присылаемые пользователем файлы мы будем архивировать на лету в zip (вдруг пользователь пришлет aspx станицу на которой сможет выполнить свой код), а ограничивать его по расширению файла мы не хотим. Стоит отметить, что в рамках стандартного функционала дистрибутива можно ограничивать загрузку исполняемых файлов в какую-либо директорию, в нашем случае это папка "/upload".
Немного теории
У каждого элемента инфоблока есть набор стандартных полей (они жестко прописаны в базе данных и не могут быть удалены или добавлены), такие как название элемента, текст описания, дата начала, дата окончания активности и прочее. Так же, мы можем добавлять собственные свойства для каждого инфоблока, с которыми, в последствии, можно будет работать как с полями. Для нашего примера мы будем изменять логику работы контролов, отвечающих за поле (привязка к разделам) и за свойство (загружаемого файла).
Подготовка
1) Создаем инфоблок (с символьным кодом "files") со свойством типа "Файл" (символьный идентификатор FILE)
2) Добавляем в корень 3 секции: технология (указываем "Символьный код : bytechnology"), язык программирования(указываем "Символьный код : bylang") и категория(указываем "Символьный код : bycategory")
3) Заполняем эти три категории подразделами, к которым уже будет привязан каждый загружаемый файл
4) Создаем страницу и бросаем на неё наш компонент Формы добавления / редактирования (iblock.element.webform), в настройках убираем вывод таких необязательных полей, как: картинка для привью, детальная картинка, дата начала активности, дата окончания активности и подробный текст. В результате у нас должно получиться следующее:
Реализация
Сначала нам нужно добавить свой шаблон компонента, делается это в точности так же как в PHP версии: либо копируем через настройки компонента текущий шаблон компонента в директорию шаблона сайта,
либо делаем все то же самое руками. Идем в директорию "/bitrix/templates/%template_name%/components/bitrix/iblock.element.webform", создаем там папочку, например "new-file" и добавляем в неё два файла:
- template.ascx - отвечает за генерацию конечного HTML кода, его необходимо скопировать из оригинального шаблона компонента и внести некоторые изменения (для типа поля соответствующего секции и для типа свойства соответствующего файлу).
- template.ascx.cs - здесь будет храниться наша "магия", которая будет заключаться в изменении внешнего вида формы и логики обработки полей формы
Для того, чтобы сделать свои обработчики полей формы, необходимо создать свой класс шаблона унаследованного от IBlockElementWebFormTemplate, в котором задать свои обработчики для стандартных полей инфоблока и его свойств в методе OnInit:
Как Вы правильно догадались, CreateFieldPublicEditor и CreateCustomTypePublicEditor - это методы, в которых идет обработка полей. Данные методы должны выглядеть примерно следующим образом:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (Component.IsPermissionDenied)
Bitrix.Security.BXAuthentication.AuthenticationRequired();
//обработчики полей, таких как название элемента, привязка к секциям и описание.
//Нас конечно же интересуют секции.
Component.CreateFieldPublicEditor += new EventHandler<IBlockElementWebFormComponent.FieldPublicEditorEventArgs(CreateFieldPublicEditor);
//обработчики пользовательских свойств инфоблока,
//в нашем случае это необходимо для свойства типа Файл
Component.CreateCustomTypePublicEditor += new EventHandler<IBlockElementWebFormComponent.CustomTypePublicEditorEventArgs>(CreateCustomTypePublicEditor);
}
//переопределяем обработчик для поля типа секцииКласс FileElementSections должен быть унаследован от BXIBlockElementFieldPublicEditor, а класс FileUploadCustomType от BXCustomTypePublicEdit. То есть от стандартных классов редактирования поля элемента и класса редактирования свойства элемента. Теперь поговорим подробнее об этих "родителях". Родители представляю 4 метода которые отвечают за:
protected void CreateFieldPublicEditor(object component, IBlockElementWebFormComponent.FieldPublicEditorEventArgs args)
{
//SECTIONS является идентификатором поля
if (args.ID.ToUpper() != "SECTIONS")
return;
args.PublicEditor = new FileElementSections();
}
//переопределяем обработчик для свойства типа файл
protected void CreateCustomTypePublicEditor(object component, IBlockElementWebFormComponent.CustomTypePublicEditorEventArgs args)
{
//FILE является идентификатором свойства
if (args.ID.ToUpper() != "FILE")
return;
args.PublicEditor = new FileUploadCustomType(BXCustomTypeManager.GetCustomType(args.CustomField.CustomTypeId));
}
- подгрузка данных для поля (метод Load)
- вывод поля (метод Render)
- сохранение данных из поля (метод Save)
- проверку входных данных поля (метод Validate)
Итак, давайте рассмотрим класс FileElementSections, он нам более интересен, так как позволяет творить чудеса с внешним видом инпутов формы добавления/редактирования и их содержимым. Рассмотрим методы по порядку:
public override void Load(BXIBlockElement iblockElement, BXParamsBag<object> settings)В этом методе мы вначале забираем свойства контрола, которые по большей части относятся к свойствам секций инфоблока. В конце метод смотрит - мы редактируем поле или нет, если редактируем, то сохраняем секции.
{
required = settings.ContainsKey("required") ? (bool)settings["required"] : true;
fieldTitle = settings.ContainsKey("fieldTitle") ? (string)settings["fieldTitle"] : String.Empty;
textBoxSize = settings.ContainsKey("textBoxSize") ? (int)settings["textBoxSize"] : 30;
iblockId = settings.ContainsKey("iblockId") ? (int)settings["iblockId"] : 0;
onlyLeafSelect = settings.ContainsKey("onlyLeafSelect") ? (bool)settings["onlyLeafSelect"] : false;
maxSectionSelect = settings.ContainsKey("maxSectionSelect") ? (int)settings["maxSectionSelect"] : 3;
multiple = maxSectionSelect > 1;
categories = GetFileSections("bycategory");
languages = GetFileSections("bylang");
technologies = GetFileSections("bytechnology");
if (iblockElement != null)
{
foreach (BXIBlockElement.BXInfoBlockElementSection section in iblockElement.Sections)
fieldValues.Add(section.SectionId);
}
}
Наверно, ниже приведен самый вкусный метод, так как позволяет самостоятельно менять внешний вид контрола:
public override string Render(string formFieldName, string uniqueID)
{
/* задаем вспомогательные перменные, которые нам понадобятся для построения select-ов и чекбоксов */
string fieldHeader = @"<tr field field-sections{1}""><td align=right width=30% valign=top><label class=""field-title"">{0}</label></td><td valign=top>";
string fieldFooter = "</td></tr>";
string dropDownStart = @"<select name=""{0}"" class=""custom-field-list"">";
string dropDownOption = @"<option value=""{0}""{1}>{2}</option>";
string dropDownEnd = "</select>";
string checkbox = @"<input name=""{0}"" type=""checkbox"" value=""{1}"" id=""{2}""{3}/> <label for=""{2}"">{4}</label><BR>";
StringBuilder result = new StringBuilder(String.Empty);
//Categories
//формируем список отвечающий за вывод категории в виде DropDown
result.AppendFormat(fieldHeader, @"Категория<span style=""color: red;"">*</span>", isCategorySet ? "" : " field-error");
result.AppendFormat(dropDownStart, HttpUtility.HtmlEncode(formFieldName));
result.AppendFormat(dropDownOption, 0, String.Empty, "(выберите категорию)");
for (int i = 0; i < categories.Count; i++)
{
SectionTreeItem section = (SectionTreeItem)categories[i];
result.AppendFormat(
dropDownOption,
section.Id,
fieldValues.Contains(section.Id) ? " selected=\"selected\"" : String.Empty,
section.Name
);
}
result.Append(dropDownEnd);
result.AppendFormat(fieldFooter);
//Languages
//такой же селект для языков программирования
result.AppendFormat(fieldHeader, @"Язык программирования<span style=""color: red;"">*</span>", isCategorySet ? "" : " field-error");
result.AppendFormat(dropDownStart, HttpUtility.HtmlEncode(formFieldName));
result.AppendFormat(dropDownOption, 0, String.Empty, "(выберите язык)");
for (int i = 0; i < languages.Count; i++)
{
SectionTreeItem section = (SectionTreeItem)languages[i];
result.AppendFormat(
dropDownOption,
section.Id,
fieldValues.Contains(section.Id) ? " selected=\"selected\"" : String.Empty,
section.Name
);
}
result.Append(dropDownEnd);
result.AppendFormat(fieldFooter);
//Technologies
//а технологии сделаем ка мы чек боксами
result.AppendFormat(fieldHeader, @"Технологии", "");
for (int i = 0; i < technologies.Count; i++)
{
SectionTreeItem section = (SectionTreeItem)technologies[i];
result.AppendFormat(
checkbox,
HttpUtility.HtmlEncode(formFieldName),
section.Id,
HttpUtility.HtmlEncode(formFieldName) + section.Id,
fieldValues.Contains(section.Id) ? " checked=\"checked\"" : String.Empty,
section.Name
);
}
result.AppendFormat(fieldFooter);
return result.ToString();
}
Теперь рассмотрим следующий метод, который отвечает за сохранение. Он очищает привязку к секциям для элемента инфоблока и пишет новые:
public override void Save(string formFieldName, BXIBlockElement iblockElement, BXCustomPropertyCollection properties)Ну и проверка входных данных производится так для нашего элемента:
{
if (iblockElement == null)
return;
BXIBlockElement.BXInfoBlockElementSectionCollection sections = iblockElement.Sections;
sections.Clear();
foreach (int sectionId in fieldValues)
sections.Add(sectionId);
}
public override bool Validate(string formFieldName, ICollection<string> errors)Вспомогательный класс SectionTreeItem хранит в себе нужную нам информацию о секции. Он есть в архиве с шаблоном
{
postValues = HttpContext.Current.Request.Form.GetValues(formFieldName);
if (postValues == null || postValues.Length < 1)
{
fieldValues.Clear();
errors.Add("Не указана категория файла");
return false;
}
isCategorySet = false;
isLangSet = false;
fieldValues = new List<int>(postValues.Length);
foreach (string value in postValues)
{
int sectionId;
if (!int.TryParse(value, out sectionId) || sectionId < 1)
continue;
if (categories.Contains(sectionId))
{
isCategorySet = true;
fieldValues.Add(sectionId);
}
else if (languages.Contains(sectionId))
{
isLangSet = true;
fieldValues.Add(sectionId);
}
else if (technologies.Contains(sectionId))
{
fieldValues.Add(sectionId);
}
}
if (!isCategorySet)
errors.Add(String.Format("Не указана категория файла", fieldTitle));
else if (!isLangSet)
errors.Add(String.Format("Не указан язык программирования", fieldTitle));
return isCategorySet && isLangSet;
}
Класс FileUploadCustomType построен по тому же принципу, только в нем идет сжатие файла + вызов родительских функций. Рассмотрим только тот метод, где происходит сжатие:
private BXFile ValidateFile(HttpPostedFile file, ICollection<string> errors)В этом методе сначала идет проверка по размеру файла, затем по расширение и пустоту названия файла. Если все хорошо, то мы сжимаем файл и регистрируем его в системе.
{
bool result = true;
if (maxSize > 0 && file.ContentLength > maxSize)
{
result = false;
errors.Add(HttpUtility.HtmlEncode(string.Format(
"Размер файла {0} превышает {1}",
file.FileName,
BXStringUtility.BytesToString(maxSize)
)));
}
if (allowedExtensions.Count != 0)
{
string ext = Path.GetExtension(file.FileName);
if (ext != null)
ext = ext.TrimStart('.').ToLowerInvariant();
if (!allowedExtensions.Contains(ext))
{
result = false;
errors.Add(HttpUtility.HtmlEncode(string.Format(
"Тип загружаемого файла {0} не является допустимым ({1})",
file.FileName,
string.Join(", ", allowedExtensions.ToArray())
)));
}
}
if (!result)
return null;
BXFile f = null;
string fileName = Path.GetFileName(file.FileName);
if (!String.IsNullOrEmpty(fileName))
{
if (fileName.EndsWith(".zip") || fileName.EndsWith(".rar"))
f = new BXFile(file, "files", "iblock", string.Empty);
else
//сжимаем файл из текущего
f = new BXFile(new BXZip.ZipStream(file.InputStream, fileName, ushort.MaxValue / 2), fileName + ".zip", "files", "iblock", "");
if (!BXSecureIO.CheckUpload(f.FileVirtualPath))
{
errors.Add(HttpUtility.HtmlEncode(string.Format("Недостаточно прав для загрузки файла {0}", fileName)));
return null;
}
}
return f;
}
Так будет выглядеть наша форма после кастомизации.
Фото:
В Visual Studio это делается за 5 минут, без особых знаний языка C#.