Очень часто для программы нужно предусмотреть форму авторизации перед началом работы. В идеале, для удобства использования, также нужно предусмотреть возможность сохранить пароль (чтобы не вводить его каждый раз) и вызов программы с передачей логина/пароля в качестве вводных параметров (актуально для случаев, когда у пользователя несколько учетных записей -- для запуска конкретной учетки можно создать lnk-ярлык с соответствующими параметрами). Как найболее правильно это сделать?....
Для начала вспоминаем что любая программа WinForms стартует с класса Program вызывая статический метод Main. По умолчанию в нем находится две строчки
Первая строка отвечает за предустановки внешнего вида, а вторая вызывает и "захватывает" окно (т.е. метод Run завершиться только когда закроется переданный инстанс окна MainForm). Так как всё это код самого обычного метода, мы можем его расширить любым образом:
- добавить вызов окон (как в диалоговом режиме, так и просто). Вызов в диалоговом режиме будет приостанавливать выполнение кода до момента, пока не будет закрыто окно.
- добавить любые проверки перед вызовом Application.Run
- добавить проверки и НЕ вызывать Application.Run, тем самым автоматически завершить программу.
Итого самый простой алгоритм действия для формы авторизации следующий:
- вызываем диалог формы и дожидаемся её завершения
- проверяем форму (на самом деле вариантов передачи результата авторизации больше, по сути нам нужно любым способом вернуть результат авторизации в вызывающий код) на то прошла ли авторизация успешно
- если результат ОК -- вызываем основную форму
- так как нам не нужно чтобы форма авторизации вечно висела в памяти, её вызов нужно "упаковать" в отдельный метод. Таким образом наша форма станет "локальной" и будет жить только в рамках этого метода.
Теперь рассмотрим чуть подробней саму форму авторизации. Для начала её дизайн:
Пока не обращаем внимание на флаг "запомнить". Из основного, что я бы отметил по дизайну:
- обработку нужно вешать не только на кнопку "войти", но и на enter для поля пароля (как минимум).
- поле пароля по хорошему стоит закрыть символами (свойство PasswordChar)
- опционально можно поотключать кнопки свернуть/развернуть чтобы больше походило на модальное окно
- именуем элементы не дефолтно textBox1, а более осознанно
Теперь про обработку. Можно разместить всю логику прям в форме, и просто выплевывать результат. Чуть более правильный подход -- создать ТРИ отдельных класса:
- первый будет инкапсулировать логику проверки логина/пароля и сохранения его в настройках юзера
- второй -- контекст, который будет "глобальным" для всего приложения, и хранить под каким пользователем залогинились. "Глобальность" будем реализовывать через патерн Singelton, а точнее -- тупо public static
- класс отвечающий за непосредственную проверку логина+пароля на авторизацию. Обычно это обращение по API к серверу (ну или прям БД), либо иному внешнему ресурсу. Я отдельно акцентирую на этом внимание, т.к. это долгая операция и потенциально это может привести к "зависанию" программы.
publicclass AuthService
{privatereadonly DataService _dataService =new();publicasync Task<bool> Auth(string login, string password){var user =await _dataService.SignInAsync(login, password);// for cases where we don't have an asynchronous authorization method//var user = await Task.Run(() => _db.SignIn(login, password));var result = user !=null;if(result){
UserContext.Instance.CurrentUser= user;}return result;}}publicclass UserContext
{publicstatic UserContext Instance {get;}=new();public User? CurrentUser {get;set;}publicbool IsAuthed => CurrentUser !=null;private UserContext(){}}public record User(string Login);publicclass DataService
{privatereadonly TimeSpan _delay = TimeSpan.FromSeconds(2);public User? SignIn(string login, string password){
Thread.Sleep(_delay);return CheckAndResult(login, password);}publicasync Task<User?> SignInAsync(string login, string password){await Task.Delay(_delay);return CheckAndResult(login, password);}private User? CheckAndResult(string login, string password)=> login =="admin"&& password =="Nya!"?new User(login):default;}
Чуть подробней по DataService: так как нам нужно смоделировать долгое обращение (обычно запросы несущественные, но тем не менее никто не застрахован), я просто вставляю задержки на 2сек для наглядности. Также у нас не всегда есть асинхронный метод для запроса, поэтому в нашем моке объявленно два варианта (не асинхроный вариант просто пакуем в задачу, пример закоментирован в методе Auth)
publicpartialclass AuthForm : Form
{privatereadonly AuthService _authService;public AuthForm(AuthService authService){
InitializeComponent();
_authService = authService;
DialogResult = DialogResult.None;// если окно просто закрыть, то вернет не ОК, что приведет к закрытию всей программы.}privateasyncvoid LoginButtonClick(object sender, EventArgs e)=>await SignIn();privateasyncvoid PasswordTextBoxKeyDown(object sender, KeyEventArgs e){if(e.KeyCode== Keys.Enter)await SignIn();}privateasync Task SignIn(){var login = loginTextBox.Text;var password = passwordTextBox.Text;if(!(string.IsNullOrEmpty(login)||string.IsNullOrEmpty(password))){
Enabled =false;var result =await _authService.Auth(login, password);
Enabled =true;if(result){
DialogResult = DialogResult.OK;
Close();}else
MessageBox.Show(this, "неверный логин или пароль", null, MessageBoxButtons.OK, MessageBoxIcon.Error);}}}
На самом деле это не самый кошерный подход, более правильно -- просто закрывать форму, а уже на стороне вызывающего кода проверять UserContext.IsAuthed. Это дает большую независимость от того, что UI представлен в виде конкретной формы.
Теперь добавим возможность сохранить пароль, чтобы не вводить его в следующий раз. Для этого будем использовать механизм Settings, который позволяет сохранять данные в учетной записи пользователя Windows. Правый клик по проекту в SolutionExplorer - самая нижняя вкладка "Settings" - тыкаем "создать или открыть настройки". (Второй вариант: Add - Items - справа выбираем раздел General - ищем Settings File). В настройках добавляем две записи Login и Password. Тип выбираем string, скоп -- User. Значения оставляем пустыми. Желательно оставить Internal.
Найболее юзерфрендли подход состоит в том чтобы у нас при старте программы также показывалась форма авторизации, но поля заполнялись автоматически и сама "нажималась" кнопка войти. Таким образом у нас не теряется отклик "выполняется вход" и юзер видит что программа запустилась, но дожидается ответа сервера. Добавляем в конструктор формы авторизации флаг автоматического выполнения при отображении. Также добавляем флаг "запоминать".
В сервисе авторизации добавляем метод (string Login, string Password)? LoadSecure() для вычитки логина+пароля из настроек юзера. Также добавляем сохранение при авторизации (при условии что у нас выставлен флаг "запомнить").
publicclass AuthService
{privatereadonly DataService _dataService =new();publicasync Task<bool> Auth(string login, string password, bool remember){var user =await _dataService.SignInAsync(login, password);// for cases where we don't have an asynchronous authorization method//var user = await Task.Run(() => _db.SignIn(login, password));var result = user !=null;if(result){
UserContext.Instance.CurrentUser= user;if(remember){
Settings.Default.Login= login;
Settings.Default.Password= password;
Settings.Default.Save();}}return result;}public(string Login, string Password)? LoadSecure(){var login = Settings.Default.Login;var password = Settings.Default.Password;if(!(string.IsNullOrEmpty(login)||string.IsNullOrEmpty(password)))return(login, password);returndefault;}}
В стартовом методе добавляем вначале вычитку сохраненной учетки. Если успешно -- передаем флаг автоматического входа и значения.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
staticbool Auth(string[] args){var authService =new AuthService();string? login =default;string? password =default;bool remember =false;bool auto =false;var savedData = authService.LoadSecure();if(savedData is not null){(login, password)= savedData.Value;
auto =true;}usingvar dialog =new AuthForm(authService, login, password, remember, auto);
dialog.ShowDialog();return UserContext.Instance.IsAuthed;}
Не забываем подправить вызов проверки в самой форме авторизации (нужно добавить передачу флага remember из чекбокса). Теперь последняя фича -- передача параметров. Тут тоже ничего сложного: все входящие параметры, это просто массив строк, которые передаются параметром args в метод Main. Мы просто проверяем есть ли на входе параметры, если "да" -- передаем именно их окну, а не те что сохранены. Именно такая логика нужна, т.к. пользователь явно ожидает залогиниться под указанной учеткой. Он может банально не обратить внимание что программа зашла не под нужной (а если после авторизации происходят ещё какие-то действия -- это ещё может и что-то поломать или выполнить ненужные действия). Выглядит эта проверка так
staticbool Auth(string[] args){var authService =new AuthService();string? login =default;string? password =default;bool remember =false;bool auto =false;if(args is[var _login, var _password, ..var r]){
login = _login;
password = _password;
_ =bool.TryParse(r?.FirstOrDefault(), out remember);
auto =true;}else{var savedData = authService.LoadSecure();if(savedData is not null){(login, password)= savedData.Value;
auto =true;}}usingvar dialog =new AuthForm(authService, login, password, remember, auto);
dialog.ShowDialog();return UserContext.Instance.IsAuthed;}
Теперь немного разберемся с тем, что форма блокируется вот прям вся. Это плохой подход, так как ожидание может быть существенным и желательно показывать процесс работы (например анимационую иконку). При таком подходе пользователь четко понимает что прога не повисла, а ждет чего-то. Общий принцип можно использовать следующий:
- размещаем элементы на отдельной панели в окне (либо выносим в юзер контрол)
- блокируем только эту панель
- на само окно добавляем иконку на время выполнения запроса.
- после выполнения операции убираем иконку и разблокируем окно.
С учетом того что внешний код может завалится по различным причинам, которые не должны валит всё приложение (например временно нет интернета) -- стоит обернуть операцию в try-catch-finally блок. Это позволит повторить операцию ещё раз, при этом уведомив о возникших неполадках.
Всё это можно унифицировать следующим образом:
- создаем интерфейс, указывающий что нужно блокировать и где размещать элемент отображения "ожидания". Это позволит использовать код на различных формах, а не только авторизации.
- создаем класс-помощник, который выполняет безопасный вызов операции
- саму иконку реализуем через UserControl. Это позволит безболезненно менять её внешний вид.
publicpartialclass AuthForm : Form, ISafeExecuteControl
{//.. другие поляpublic Control ContainerForLoading =>this;public Control LockControl => containerPanel;//.. конструктор и прочееprivateasync Task SignIn(){var login = loginTextBox.Text;var password = passwordTextBox.Text;var remember = rememberCheckBox.Checked;if(!(string.IsNullOrEmpty(login)||string.IsNullOrEmpty(password))){awaitthis.SafeUIExecuteAsync(async()=>{if(await _authService.Auth(login, password, remember))
Close();else
MessageBox.Show(this, "неверный логин или пароль", null, MessageBoxButtons.OK, MessageBoxIcon.Error);},
"Ошибка авторизации.");}}}
Финальный момент. После того как мы успешно выполнили авторизацию, в 99 случаев из 102 на главном окне нам тоже нужно выполнить подгрузку тех или иных данных. Т.е. опять задержки и прочее, что может вызывать дискомфорт у пользователя при работе. Как не стоит делать: лепить блокирующую загрузку в конструктор или метод Load. Это приведет к тому что после закрытия окна у нас главная форму не будет показываться (пользователь не поймет что программа продолжает работу, а не вылитела). В целом у нас уже ранее набран механизм для обеспечения юзерфрендли работы, и главное окно можно реализовать так
КАК НЕ НЕДО ДЕЛАТЬ ОКНО АВТОРИЗАЦИИ
анти-пример 1. В сети оооочень часто встречается подход следующего вида:
- в качестве основной формы в Application.Run передаем форму авторизации
- после входа, мы делаем Hide() диалоговой формы, и new MainForm().Show()
Что влечет за собой такой говнокод:
- во-первых, у нас в памяти теперь висит диалог авторизации. "Ну это же немного" будете рассказывать в очередном сраче, что винда жрет слишком много ОЗУ.
- во-вторых и основное: выход из приложения теперь подразумевает грохнуть скрытую форму, до которой ещё нужно достучаться. Придется лепить всякие Application.Exit(), либо тянуть хвостом что "вот эту форму нужно сделать Close".
- в-третьих, у вас появляется супер тупая зависимость "второстепенная форма порождает главную". Когда вам нужно будет повторно вызвать форму авторизации (скажем истекло время сессии) -- нужно "поднять" заныканую форму вначале.
анти-пример 2. Засунуть создание диалоговой формы авторизации в конструктор или метод Load главной. Пример менее говняный, нежели предыдущий, тем не менее у нас появляется зависимость "главное окно знает о существовании второстепенного". Жить можно, но рекомендую не порождать супер-окон (как и божественных классов).
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
На странице:
https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/
нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов.
. . .
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Статья исключительно для начинающих. Подходы оригинальностью не блещут.
В век Веб все очень привыкли к дизайну Single-Page-Application .
Быстренько разберем подход "на фреймах".
Мы делаем одну. . .
— Расскажи мне о Мире, бродяга,
Ты же видел моря и метели.
Как сменялись короны и стяги,
Как эпохи стрелою летели.
- Этот мир — это крылья и горы,
Снег и пламя, любовь и тревоги,
И бескрайние. . .
Модуль PowerShell 5. 1+ : Snippets. psm1
У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\
А в самом низу файла-профиля. . .