Форум программистов, компьютерный форум, киберфорум
Wolfdp
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

[Пример для начинающих] Форма авторизации на WinForms

Запись от Wolfdp размещена 30.11.2023 в 00:50
Показов 11823 Комментарии 0
Метки .net, c#, winforms

Очень часто для программы нужно предусмотреть форму авторизации перед началом работы. В идеале, для удобства использования, также нужно предусмотреть возможность сохранить пароль (чтобы не вводить его каждый раз) и вызов программы с передачей логина/пароля в качестве вводных параметров (актуально для случаев, когда у пользователя несколько учетных записей -- для запуска конкретной учетки можно создать lnk-ярлык с соответствующими параметрами). Как найболее правильно это сделать?....

примечаниеВсе примеры будут приводится на .net core 8, с использованием синтаксиса C# 12. Проект набирался в VisualStudio 2022. Также дизайн редактировался на мониторе с масштабированием 200%, так что открывая пример на обычном мониторе вполне возможны искажения в ту или иную сторону (например текст может быть слишком мелким, или окно слишком большое/маленькое).


Для начала вспоминаем что любая программа WinForms стартует с класса Program вызывая статический метод Main. По умолчанию в нем находится две строчки

C#
1
2
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
Первая строка отвечает за предустановки внешнего вида, а вторая вызывает и "захватывает" окно (т.е. метод Run завершиться только когда закроется переданный инстанс окна MainForm). Так как всё это код самого обычного метода, мы можем его расширить любым образом:
- добавить вызов окон (как в диалоговом режиме, так и просто). Вызов в диалоговом режиме будет приостанавливать выполнение кода до момента, пока не будет закрыто окно.
- добавить любые проверки перед вызовом Application.Run
- добавить проверки и НЕ вызывать Application.Run, тем самым автоматически завершить программу.

Итого самый простой алгоритм действия для формы авторизации следующий:
- вызываем диалог формы и дожидаемся её завершения
- проверяем форму (на самом деле вариантов передачи результата авторизации больше, по сути нам нужно любым способом вернуть результат авторизации в вызывающий код) на то прошла ли авторизация успешно
- если результат ОК -- вызываем основную форму
- так как нам не нужно чтобы форма авторизации вечно висела в памяти, её вызов нужно "упаковать" в отдельный метод. Таким образом наша форма станет "локальной" и будет жить только в рамках этого метода.

C#
1
2
3
4
5
6
7
8
9
10
ApplicationConfiguration.Initialize();
if (Auth())
    Application.Run(new MainForm());
 
static bool Auth()
{
    using var dialog = new AuthForm();
    dialog.ShowDialog();
    return dialog.DialogResult == DialogResult.OK;
}
Теперь рассмотрим чуть подробней саму форму авторизации. Для начала её дизайн:
Нажмите на изображение для увеличения
Название: auth_form.jpg
Просмотров: 377
Размер:	91.0 Кб
ID:	8376

Пока не обращаем внимание на флаг "запомнить". Из основного, что я бы отметил по дизайну:
- обработку нужно вешать не только на кнопку "войти", но и на enter для поля пароля (как минимум).
- поле пароля по хорошему стоит закрыть символами (свойство PasswordChar)
- опционально можно поотключать кнопки свернуть/развернуть чтобы больше походило на модальное окно
- именуем элементы не дефолтно textBox1, а более осознанно

Теперь про обработку. Можно разместить всю логику прям в форме, и просто выплевывать результат. Чуть более правильный подход -- создать ТРИ отдельных класса:
- первый будет инкапсулировать логику проверки логина/пароля и сохранения его в настройках юзера
- второй -- контекст, который будет "глобальным" для всего приложения, и хранить под каким пользователем залогинились. "Глобальность" будем реализовывать через патерн Singelton, а точнее -- тупо public static
- класс отвечающий за непосредственную проверку логина+пароля на авторизацию. Обычно это обращение по API к серверу (ну или прям БД), либо иному внешнему ресурсу. Я отдельно акцентирую на этом внимание, т.к. это долгая операция и потенциально это может привести к "зависанию" программы.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class AuthService
{
    private readonly DataService _dataService = new();
 
    public async 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;
    }
}
 
public class UserContext
{
    public static UserContext Instance { get; } = new();
 
    public User? CurrentUser { get; set; }
 
    public bool IsAuthed => CurrentUser != null;
 
    private UserContext() { }
}
 
public record User(string Login);
 
public class DataService
{
    private readonly TimeSpan _delay = TimeSpan.FromSeconds(2);
 
    public User? SignIn(string login, string password)
    {
        Thread.Sleep(_delay);
        return CheckAndResult(login, password);
    }
 
    public async 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)

использование внутри формы

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public partial class AuthForm : Form
{
    private readonly AuthService _authService;
 
    public AuthForm(AuthService authService)
    {
        InitializeComponent();
        _authService = authService;
        DialogResult = DialogResult.None; // если окно просто закрыть, то вернет не ОК, что приведет к закрытию всей программы.
    }
 
    private async void LoginButtonClick(object sender, EventArgs e)
        => await SignIn();
    private async void PasswordTextBoxKeyDown(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Enter)
            await SignIn();
    }
 
    private async 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.
Нажмите на изображение для увеличения
Название: settings.jpg
Просмотров: 695
Размер:	527.8 Кб
ID:	8377

примечаниеНа самом деле механизм сохранения можно писать свой. Тем не менее общий подход -- сохранять в папке AppData, как правило либо Local\MySoftName, либо Roaming\MySoftName. Во-первых это позволяет разным пользователя не конфликтовать между собой. Во-вторых папка AppData по умолчанию доступна только владельцу (админ может только добавить себя в разрешенные), что страхует от чтения/записи посторонними.


Найболее юзерфрендли подход состоит в том чтобы у нас при старте программы также показывалась форма авторизации, но поля заполнялись автоматически и сама "нажималась" кнопка войти. Таким образом у нас не теряется отклик "выполняется вход" и юзер видит что программа запустилась, но дожидается ответа сервера. Добавляем в конструктор формы авторизации флаг автоматического выполнения при отображении. Также добавляем флаг "запоминать".

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public partial class AuthForm : Form
{
    private readonly AuthService _authService;
    private bool autoLogin;
 
    public AuthForm(AuthService authService, string? login, string? password, bool remember, bool auto)
    {
        InitializeComponent();
        _authService = authService;
        loginTextBox.Text = login;
        passwordTextBox.Text = password;
        rememberCheckBox.Checked = remember;
        autoLogin = auto;
    }
 
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        if (autoLogin)
        {
            autoLogin = false;
            _ = SignIn();
        }
    }
}
В сервисе авторизации добавляем метод (string Login, string Password)? LoadSecure() для вычитки логина+пароля из настроек юзера. Также добавляем сохранение при авторизации (при условии что у нас выставлен флаг "запомнить").

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class AuthService
{
    private readonly DataService _dataService = new();
 
    public async 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);
        return default;
    }
}
В стартовом методе добавляем вначале вычитку сохраненной учетки. Если успешно -- передаем флаг автоматического входа и значения.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static bool 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;
    }
 
    using var dialog = new AuthForm(authService, login, password, remember, auto);
    dialog.ShowDialog();
    return UserContext.Instance.IsAuthed;
}
Не забываем подправить вызов проверки в самой форме авторизации (нужно добавить передачу флага remember из чекбокса). Теперь последняя фича -- передача параметров. Тут тоже ничего сложного: все входящие параметры, это просто массив строк, которые передаются параметром args в метод Main. Мы просто проверяем есть ли на входе параметры, если "да" -- передаем именно их окну, а не те что сохранены. Именно такая логика нужна, т.к. пользователь явно ожидает залогиниться под указанной учеткой. Он может банально не обратить внимание что программа зашла не под нужной (а если после авторизации происходят ещё какие-то действия -- это ещё может и что-то поломать или выполнить ненужные действия). Выглядит эта проверка так

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static bool 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;
        }
    }
 
    using var dialog = new AuthForm(authService, login, password, remember, auto);
    dialog.ShowDialog();
    return UserContext.Instance.IsAuthed;
}
Теперь немного разберемся с тем, что форма блокируется вот прям вся. Это плохой подход, так как ожидание может быть существенным и желательно показывать процесс работы (например анимационую иконку). При таком подходе пользователь четко понимает что прога не повисла, а ждет чего-то. Общий принцип можно использовать следующий:
- размещаем элементы на отдельной панели в окне (либо выносим в юзер контрол)
- блокируем только эту панель
- на само окно добавляем иконку на время выполнения запроса.
- после выполнения операции убираем иконку и разблокируем окно.

С учетом того что внешний код может завалится по различным причинам, которые не должны валит всё приложение (например временно нет интернета) -- стоит обернуть операцию в try-catch-finally блок. Это позволит повторить операцию ещё раз, при этом уведомив о возникших неполадках.

Всё это можно унифицировать следующим образом:
- создаем интерфейс, указывающий что нужно блокировать и где размещать элемент отображения "ожидания". Это позволит использовать код на различных формах, а не только авторизации.
- создаем класс-помощник, который выполняет безопасный вызов операции
- саму иконку реализуем через UserControl. Это позволит безболезненно менять её внешний вид.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
internal interface ISafeExecuteControl
{
    Control ContainerForLoading { get; }
    Control? LockControl { get; }
}
 
internal static class Helper
{
    public static async Task SafeUIExecuteAsync(
        this ISafeExecuteControl control,
        Func<Task> task,
        string errorMessage = "runtime error")
    {
        var parent = control.ContainerForLoading;
 
        if(control.LockControl is not null)
            control.LockControl.Enabled = false;
        using var loading = new LoadingControl
        {
            Anchor = AnchorStyles.None
        };
        var x = parent.ClientSize.Width / 2 - loading.Width / 2;
        var y = parent.ClientSize.Height / 2 - loading.Height / 2;
        loading.Location = new Point(x, y);
        parent.Controls.Add(loading);
        parent.Controls.SetChildIndex(loading, 0);
 
        try
        {
            await task();
        }
        catch
        {
            MessageBox.Show(parent, errorMessage, "error", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        finally
        {
            parent.Controls.Remove(loading);
            if (control.LockControl is not null)
                control.LockControl.Enabled = true;
        }
    }
}

Реализация всего это в форме авторизации
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public partial class AuthForm : Form, ISafeExecuteControl
{
    //.. другие поля
 
    public Control ContainerForLoading => this;
    public Control LockControl => containerPanel;
 
    //.. конструктор и прочее
 
    private async Task SignIn()
    {
        var login = loginTextBox.Text;
        var password = passwordTextBox.Text;
        var remember = rememberCheckBox.Checked;
        if (!(string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)))
        {
            await this.SafeUIExecuteAsync(async () =>
            {
                if (await _authService.Auth(login, password, remember))
                    Close();
                else
                    MessageBox.Show(this, "неверный логин или пароль", null, MessageBoxButtons.OK, MessageBoxIcon.Error);
            },
            "Ошибка авторизации.");
        }
    }
}
Финальный момент. После того как мы успешно выполнили авторизацию, в 99 случаев из 102 на главном окне нам тоже нужно выполнить подгрузку тех или иных данных. Т.е. опять задержки и прочее, что может вызывать дискомфорт у пользователя при работе. Как не стоит делать: лепить блокирующую загрузку в конструктор или метод Load. Это приведет к тому что после закрытия окна у нас главная форму не будет показываться (пользователь не поймет что программа продолжает работу, а не вылитела). В целом у нас уже ранее набран механизм для обеспечения юзерфрендли работы, и главное окно можно реализовать так

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using Nya.AuthExample.Forms.UserControls;
using Nya.AuthExample.Services;
namespace Nya.AuthExample.UI.Forms;
 
public partial class MainForm : Form, ISafeExecuteControl
{
    private readonly DataService _dataService;
 
    public Control ContainerForLoading => this;
    public Control? LockControl => null;
 
    public MainForm()
    {
        InitializeComponent();
        _dataService = new();
    }
 
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        _ = this.SafeUIExecuteAsync(LoadData, "Ошибка загрузки данных");
    }
 
    private async Task LoadData()
    {
        var data = await _dataService.Load();
 
        #region update view
 
        SuspendLayout();
 
        var width = ClientSize.Width - 45;
        foreach (var item in data)
        {
            var itemControl = new DataItemControl(item)
            {
                Width = width
            };
            dataFlowLayoutPanel.Resize += (_, a) =>
            {
                int w = dataFlowLayoutPanel.ClientSize.Width - 45;
                int h = itemControl.Size.Height;
                itemControl.Size = new Size(w, h);
            };
            dataFlowLayoutPanel.Controls.Add(itemControl);
        }
 
        ResumeLayout(false);
        PerformLayout();
 
        #endregion
    }
}
Финальный код примера в архиве Nya.AuthExample.zip

КАК НЕ НЕДО ДЕЛАТЬ ОКНО АВТОРИЗАЦИИ
анти-пример 1. В сети оооочень часто встречается подход следующего вида:
- в качестве основной формы в Application.Run передаем форму авторизации
- после входа, мы делаем Hide() диалоговой формы, и new MainForm().Show()

Что влечет за собой такой говнокод:
- во-первых, у нас в памяти теперь висит диалог авторизации. "Ну это же немного" будете рассказывать в очередном сраче, что винда жрет слишком много ОЗУ.
- во-вторых и основное: выход из приложения теперь подразумевает грохнуть скрытую форму, до которой ещё нужно достучаться. Придется лепить всякие Application.Exit(), либо тянуть хвостом что "вот эту форму нужно сделать Close".
- в-третьих, у вас появляется супер тупая зависимость "второстепенная форма порождает главную". Когда вам нужно будет повторно вызвать форму авторизации (скажем истекло время сессии) -- нужно "поднять" заныканую форму вначале.

анти-пример 2. Засунуть создание диалоговой формы авторизации в конструктор или метод Load главной. Пример менее говняный, нежели предыдущий, тем не менее у нас появляется зависимость "главное окно знает о существовании второстепенного". Жить можно, но рекомендую не порождать супер-окон (как и божественных классов).
Метки .net, c#, winforms
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
PowerShell Snippets
iNNOKENTIY21 11.11.2025
Модуль PowerShell 5. 1+ : Snippets. psm1 У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\ А в самом низу файла-профиля. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru