Как стать автором
Обновить

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

Хм, а не проще ли справа использовать список с чекбоксами? ИМХО, будет понятнее без кнопочек добавить/удалить.
Ну это немного оффтоп, мы тут не о дизайне окна говорим :) Если ролей 10, то конечно лучше с чекбоксами, если штук 100 и более, то пусть уж лучше через отдельное окно, там хоть контекстный поиск есть по части названия :)
Спасибо за статью!

1) Так всё таки, вы пишете этот код (например заполнение ListView в цикле по списку объектов) для каждого списочного контрола формы, или есть свои наработки по уходу от этой рутинной работы?

2) Используете ли гриды для редактирования? Если да, то алгоритм работы аналогичен?

3) небольшой оффтоп, а как в дальнейшем используете этот список присвоенных ролей и прав? Вызываете в начале каждого метода функцию проверки, с именем пользователя и названием/ID метода?
Ну в общем-то подход довольно стандартный для Delphi.
Извините, но очень уж хочется привести сравнение с реализацией на WPF. Тут прямо идеальный случай. Понимаю, что это оффтопик, но может покажется кому-то интересным.

Примерный код окна:
<Window>
<Grid>
<ListBox Name=«lbEmployees»/>
<ListBox ItemsSource="{Binding ElementName=lbWorkers, Path=SelectedItem.Roles}" Name=«lbRoles»/>
<Button Click=«AddRoleClick»/>
<Button Click=«RemoveRoleClick»/>
</Grid>
</Window>

Код приложения:

public sealed class EmployeeRole
{
private string m_Name;

public EmployeeRole(string name)
{
m_Name = name;
}

public override string ToString()
{
return m_Name;
}
}

public sealed class Employee
{
private string m_Name;
private ObservableCollection m_Roles;

public Employee(string name, params EmployeeRole[] roles)
{
m_Name = name;
m_Roles = new ObservableCollection(roles);
}

public ObservableCollection Roles
{
get { return m_Roles; }
}

public sealed override string ToString()
{
return m_Name;
}
}

public partial class MyWindow: Window
{
public static readonly ReadOnlyCollection m_Roles =
new ReadOnlyCollection(
new EmployeeRole[] {
new EmployeeRole(«Бухгалтер»),
new EmployeeRole(«Администратор»),
new EmployeeRole(«Планктон») });

private List GetEployeeList()
{
var result = new List(3);
result.Add(new Employee(«Иванов», m_Roles[0], m_Roles[1]));
result.Add(new Employee(«Петров», m_Roles[1], m_Roles[2]));
result.Add(new Employee(«Сидоров», m_Roles[0], m_Roles[2]));
return result;
}

public MyWindow()
{
InitializeComponent();
lbWorkers.ItemsSource = GetEployeeList();
}

public void AddRoleClick(Object sender, RoutedEventArgs e)
{
var employee = lbEmployees.SelectedItem as Employee;
if (null == employee) return;
var roles = QueryAddRoles();

lbRoles.SelectedItems.Clear();
foreach (var role in roles)
{
employee.Roles.Add(role);
lbRoles.SelectedItems.Add(role);
}
}

public void RemoveRoleClick(Object sender, RoutedEventArgs e)
{
var employee = lbEmployees.SelectedItem as Employee;
if (null == employee) return;
var roles = lbRoles.SelectedItems;
if (null == roles || 0 == roles.Count) return;

foreach (EmployeeRole role in roles)
employee.Roles.Remove(role);
}
}

Практически аналог вашего кода. Кто-то там в комментариях к предыдущему топику говорил про кучу кода.
Сожрались скобки в ObservableCollection<EmployeeRole> и List<Employee>.
Пожалуйста, в следующий раз через тэг source и под spoiler, иначе тяжело переваривать.
Не тяжело — не работает. Теги я всегда ставлю, с надеждой на светлое будущее. Я тут типа персона нон грата. Так что если вам не тяжело, перепостите сами.
Спасибо за пример реализации на другом языке.) Это как раз то, чего не хватало. Хотя моя логика все-таки ближе к MVC, чем ваша. Мне удается избежать вот такого:
foreach (var role in roles)
{
employee.Roles.Add(role);
lbRoles.SelectedItems.Add(role);
}
когда нужно параллельно изменять и данные, и интерфейс. У вас в AddRoleClick и в RemoveRoleClick код написан таким образом, что потерять синхронизацию GUI с данными очень легко. Предположите, добавляется еще одна кнопка «Удалить каждую вторую роль» (тупой пример, понимаю). Вам опять придется отдельно удалять роли из employee.Roles и отдельно из lbRoles. А у меня этого делать не нужно, обновление списка ролей произойдет автоматически.

!!! Пока писал этот комментарий понял свою ошибку: я в статье не показал, что при присвоении SelUser.Roles := NewRoles обновление списка ролей также происходит автоматически. Упустил я этот важный момент. Действительно, метод SetRoles класса TUser должен вызывать event, приводящий либо к FillRoles, либо к UpdateSelUser. Это как раз тема для третьей части статьи.

Но в целом ваш код, действительно, эквивалентен моему.
Да не за что. В том вся и фишка, что Add/Remove происходит только в модели. И момента lbRoles.SelectedItems.Add(role); можно было избежать, если ввести в модель свойство IsSelected. Тогда бы код выглядел так:

foreach (var role in roles)
{
employee.Roles.Add(role);
role.IsSelected = true;
}

Но в XAML надо было бы добавить стиль контейнера для списка lbRoles (тут сложновато писать XML).
FSelUserID := -2; // Хочу, чтобы сработал Set-метод
SelUserID := -1; // По умолчанию не выбираю никакого пользователя
Зачем плодить костыли на пустом месте?
Если нужно иметь возможность выполнить некий код минуя проверку, лучше вынеси его в отдельный метод, а не заниматься плясками с бубном.
Это не костыль. Это инициализация property со срабатыванием setter'а.
Это всё же костыль. Костыль для вызова кода, который происходит при срабатывании сеттера. Хак, завязаный на инсайдерскую информацию о внутренней логики сеттера и прямой доступ к полю. Вам советуют в сеттере делать так:

if FSelUserID  <> Value then 
  InternalSetSelUserID(Value)


а при инициализации вызывать InternalSetSelUserID(-1)
В старых дельфях (с однозначным номером) все толковые гриды заточены под использование DataSource, фактически таблицы БД. В принципе, можно использовать таблицы без БД (в памяти), есть куча примеров и готовых программ.

Сам я не люблю DataGrid'ы, они для меня недостаточно удобны и слишком сложны. Я делаю примерно как вы, отображение виртуальной таблицы в виде ObjectList'а в в какую-нибудь простую визуальную таблицу. Но ObjectList при этом не простой, в нем есть описание колонок и данных в колонках. И визуальная таблица тоже не совсем простая, умеет привязываться к виртуальной таблице.
у вас свой собственный ORM, я правильно понимаю?
1. Master-detail называется такое отношение частей интерфейса, как в вашем примере.

2. Забудьте про with, на них часто бывают проблемы с отладкой, они ухудшают читаемость кода. С ними не всегда понятно какого объекта используется атрибут (в случае с вложенными with, ...).

3. Поддерживаю критику TfmUserRights.FormCreate.

4. (Поэтому в событии OnClick списка lbUsers || TfmUserRights.lbUsersClick ??????). Если я все правильно понял, то:
Вот тут у вас единственна серьезная ошибка, которую я нашел. Вы не сделали обратную синхронизацию, а сделали лишь ложную видимость такой синхронизация. Дело в том, что пользователь может выбрать item не только кликом мышки, но и клавиатурой, и еще каким-то over-хитрым способом, и тогда у вас получится десинхронизация и ложные вводы.

Я так подозреваю, это только потому что у lbUsers компонента (ListView?) нет события onChangeSelection?

P. S. Благодарю за статью, было интересно и познавательно.
И еще одно, если вы будете выкладывать .exe и исходники, вероятность недоразумений уменьшится, а вероятность появления серьезной критики / умного комментария — увеличится.

P. S. Хотел поставить плюсик за статью, случайно промазал и поставил минус, а передумать оно не дает((. Это случайность, простите.
Позвольте не согласиться с п.2 про 'with', ибо разумное [тут несколько критериев, один из которых отсекает ваш пример с непонятками владельца аттрибута] его использование, как раз наоборот, повышает читабельность кода и, как бы это не банально звучало, слегка увеличивает производительность (посмотрите, что генерирует dcc32 от D2007, к примеру, в том или ином случае).
| Позвольте не согласиться
Не позволю! :)

Разумное использование — в epsilon случаях, все остальные — убивают. И тут основной момент, что то, что сейчас разумно:
with TPanel.Create(self) do
begin
  x := 1;
  y := 1;
end;

имеет свойство становиться глупым со временем:
with TPanel.Create(self) do
begin
   x := 1;
   y := 1;
   with TButton.Create(self) do
   begin
       x := 10; 
       y := 10;
   end;
end;


И этого сложно избежать. А по тому, лучше все-же не использовать.
Даже в первом случае это близко к клинике. Второй случай, само собой. Есть же корпоративные (индивидуальные, в конце концов) гайды, вида: 'with' обрамляем не менее n-аттрибутов; не допускаются вложенные 'with' конструкции и т.д. С таким же успехом можно навалиться на 'using' в C# и т.д.

Количество разумных использований действительно мало, но именно в тех ситуациях это дает профит как визуально, так и с точки зрения производительности. Собственно о чем и речь. «Забудьте про with» — это хреновая идея.
Согласен.
Ну слава богу!
Бог тут ни при чем)
Нарвался недавно на занятную проблему с with Rect do в используемой библиотеке при переходе с нею на Delphi XE2: там в TRect оказались методы, и один из методов — Size. Он стал конфликтовать с локальной переменной, и таких мест оказалось заметное количество :)

Пришлось для минимизации правок переименовывать Size в LocalSize…

Правда, так и не уверен, что остальные имена на то что нужно теперь ссылаются…
1. Спасибо, я в курсе.

2. Да, проблемы с with иногда приводят к трудновыявляемым ошибкам из-за двойственности. Но в примитивных случаях все-равно он удобен. В C# лучше — он выдает ошибку при такой двойственности.

3. Можно было напрямую вызвать SetSelUserID(-1), но при этом FSelUserID желательно все-равно визуально проинициализировать, хоть мы и знаем, что он инициализируется нулем при инстанцировании класса. Просто чтобы было видно, что про инициализацию этого поля мы не забыли.

4. У TListBox событие OnClick срабатывает при любом способе выделения элеиментов, как мышкой, так и клавиатурой. Так что никакой серьезной ошибки тут нет.

На серьезную ошибку статьи не указал НИКТО. В статье не показано, за счет чего обновляется список ролей при добавлении/удалении ролей текущему сотруднику кнопками Добавить/Удалить. При текущем коде роли будут к объектам TUser добавляться и удаляться, но список ролей в GUI не обновится до тех пор, пока вы не переключитесь с одного пользователя на другого. В следующей части статьи я напишу об этом.

>если вы будете выкладывать .exe и исходники
Вам они действительно так уж нужны для понимания того, что я хочу донести? Признаться честно, у меня их нет. Код я сочинил из головы на основе опыта написания подобного кода десятки (а то и сотни) раз. И форму я просто «нарисовал» в Delphi. В проектах такой формы нет. Понятно, что поскольку код не запускался и не тестировался, в нем могут быть неточности. Но я надеюсь, что приведенного кода вполне достаточно, чтобы понять саму идею, которую я пытался донести. Идея состоит в том, чтобы вы всегда работали с моделью, а элементы интерфейса менялись бы в качестве реакции на изменение модели. Я никогда не пытаюсь изменить интерфейс параллельно с моделью и не пытаюсь подменить модель интерфейсом. Интерфейс — это лишь отображение модели.

4.1 Какой хитрый TListBox)).

4.2 btAddRoleClick, btDeleteRoleClick — мне показалось что в конце этих методов есть UpdateSelRoles… Собственно и не странно, что НИКТО не заметил, исходников то нет, приложение потыкать нельзя, а включал глубокий мат. анализ — не факт, что целесообразно. Да оно и понятно как сделать, но конечно-же будет интересно почитать, жду).

4.3. Нет, мне даже кажется что я за 5 лет до, понял что вы хотите до меня донести). Признаюсь, исходники выглядят так, будто у вас они таки есть).

5. btDelRoleClick и прочие друзья. В событиях графических компонентов делфи, за редким исключением, строго не рекомендуется писать код. Потому, что одно действия может быть вызвана разными методами (onClick, onKeyPress, onToolbarPress… что там еще есть). Впрочем, я почему-то уверен, что вы и без меня об этом знаете.
2. Да, проблемы с with иногда приводят к трудновыявляемым ошибкам из-за двойственности. Но в примитивных случаях все-равно он удобен.
В чём, кстати, удобство?
Конструкция затрудняет анализ кода, да ещё и создаёт почву для ошибок при появлении новых полей в классе объекта, использованного в качестве аргумента к with.

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

А еще, примитивные случаи имею свойства жиреть со временем.
*имеют
>В чем, кстати, удобство?
В длине строки :)
Часто, например, такой код встречается:
with TSomeForm.Create do
begin
SomeLabel.Caption := 'Изменение сотрудника: ' + User.Name;
SomeButton.Enabled := False;
SomeOtherButton.Enabled := True;
Show;
end;
Здесь with позволил мне не заводить доп. переменную и не писать 4 раза ее название в начале каждой строки.
Здесь with позволил мне не заводить доп. переменную и не писать 4 раза ее название в начале каждой строки.
Почему бы и не написать имя переменной четыре раза?
Это ведь совсем не сложно, а код гораздо читабельнее станет.

Кстати, у вас молоко убежало утечка в этом примере.
Имелось в виду, что форма не модальная, а в OnClose написано Action := caFree
Ну что вы как дети, ей богу.
в OnClose написано Action := caFree

Согласитесь, из приведённого фрагмента это никак не следовало.
Согласен :) Хотя можно было попробовать догадаться, например, по тому факту, что там стоял вызов Show, а не ShowModal. Я это лишь к тому, что к мелочам можно и не придираться уж так.
Чего вы пристали к 'with'? Это вполне удобоваримый сахар (имеющийся в куче языков), который несет в себе как синтаксическую радость (следовательно визуальную), так и генерирует чуть более оптимальный код в ряде случаев. То, что его использование может приводить к глупым ошибкам исключительно на совести разработчика, ровно, как и использование любой другой языковой конструкции.
В больших и сложных проектах вред от такого сахара превышает пользу.
Брррх, мы вроде оба в адеквате, но как можно оценивать вред / пользу конструкции в зависимости от габарита проекта? Вероятность удачного (lol) использования 'with' (да и вообще любой, черт побери, конструкции) прямо пропорциональна вашему знанию языка и умению этим всем добром пользоваться. Со временем ты вырабатываешь для себя правила когда стоит использовать ту или иную конструкцию, когда можно заменить ее альтернативой или отказаться вообще. Когда стоит обратить внимание на тело конструкции или придумать специальный трик, который позволял бы писать код не задумываясь о проблемах (да и проблемы эти надуманы, отлично понимаю о чем вы говорите, но если честно, более чем за 10 лет работы с Delphi не вспомню ни одного головняка с 'with', так уж сложилось.

Здесь же не идет речь о вечном холиваре про goto, который меняет control flow тупо переставлением EIP (хотя даже в том холиваре находятся адекватные аргументы со стороны аккуратных сторонников).
Возможны всякие неприятности при копипасте и рефакторинге кода. Я даже в коде методов классов self постоянно использую как префикс, а то были случаи путаницы. И вообще, влияние имен переменных и оформления кода на вероятность проблем гораздо выше, чем многие думают.

Берите пример с Python — там правила хорошего тона уже в синтаксис заложены.
Полностью соглашусь про имена переменных, оформление кода (как форматирование, так и выбор конструкций) и даже про self в каком-то роде. Но хоть убейте, я не вижу аргументации на предмет того, чем использование 'with' страшнее циклов 'for' (в которых тоже можно допустить кучу проблем, начиная от типа счетчика, заканчивая байками из 90х про eval правой границы), уж простите за примерчик.
with затрудняет чтение кода, больше информации нужно держать в голове, при отладке стека вызовов читать код с начала блока. А польза от него весьма незначительная.

goto тоже вреден тем, что портит логическую структуру программы. И очень вреден при неумелом использовании внутри try… finally блоков.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации