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

[Пример для начинающих] Singel-form или навигация в WinForms через UserControl

Запись от Wolfdp размещена 24.12.2023 в 10:47
Показов 1907 Комментарии 0

Для начинающих знакомство с WinForms рано или поздно появляется ситуация, когда нужно пользователю показывать разные "страницы" приложения, в зависимости от его действий. Банальны пример: список и возможность редактирования отдельной записи. Самое первое что приходит на ум -- просто переключатся между окнами. Вот только такой подход имеет определенные проблемы:
  • главная проблема -- в Application.Run уже находится главная форма, которая определяет всю работу приложения. Её либо скрывать и хранить где-то в закромах для доступа, либо постоянно лезть в ApplicationContext и переназначать MainForm. Последний подход опасен тем что забыв переназначить, мы либо тупо грохним приложение послез закрытия текущей формы, либо оставим висеть программу в процессах.
  • визуально окна будут мигать, появляться в рандомных местах, разных размеров и прочее. Да, можно написать код, который будет получать размеры и позицию закрываемого окна и назначать новому, но это дополнительный простор для багов.

Чтобы упростить себе жизнь, можно не прыгать по формам, а просто оперировать UserControl. Вообще новички почему-то очень редко юзают этот инструмент, который очень и очень упрощает проектирование приложения. В частности с помощью него легко можно:
  • показывать кастомный список с любым дизайнерским оформлением, а не ограничиваться exel-подобным гридом
  • дробить сложный UI на отдельные части
  • переиспользовать в качестве partial
  • в целом строить логику на оперировании контролами (в том числе и навигации по приложению)

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

примечание 2Я не являюсь профессиональным разработчиком на WinForms. Данный пример -- исключительно попытка показать возможности UserControl и альтернативу ситуации со 100500 окнами. Для построения действительно правильной архитектуры в комерческих приложениях стоит ознакомится с профильной литературой и материалом от более матерых WinForms-developer


Для начала разберем самый примитивный пример: одна форма, два юзер-контрола и переключения между ними. Создаем два UserControl, на которых будет по Label (чтобы различать их) и кнопка перехода на другой контрол.
Нажмите на изображение для увеличения
Название: Two_UserControl.jpg
Просмотров: 277
Размер:	54.4 Кб
ID:	8419

Далее, создаем класс, который будет управлять размещение контролов на главной форме. Он будет стоять из следующих частей:
  • метод регистрации главного окна
  • метод, позволяющий зачистить текущий контрол на форме (для простоты примера я выбрал грубый подход, когда тупо сносим все контролы)
  • методы, размещающие требуемые новые контролы.
  • для доступа из любой точки приложения -- singelton в виде статического свойства.

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
internal class UIManager
{
    private Form? mainForm;
 
    public static UIManager Instance { get; } = new();
 
    private UIManager() { }
 
    public void RegMainForm(Form form)
        => mainForm = form;
 
    public void ShowAdminUserControl()
    {
        CheckRegMainForm();
        DropOld();
        var controls = mainForm!.Controls;
        controls.Add(new AdminUserControl
        { 
            Dock = DockStyle.Fill
        });
    }
    public void ShowClientUserControl()
    {
        CheckRegMainForm();
        DropOld();
        var controls = mainForm!.Controls;
        controls.Add(new ClientUserControl
        {
            Dock = DockStyle.Fill
        });
    }
 
    private void DropOld()
    {
        var controls = mainForm!.Controls;
        var olds = controls.Cast<Control>().ToArray();
        controls.Clear();
        foreach (Control control in olds)
            control.Dispose();
    }
    private void CheckRegMainForm()
    {
        if (mainForm is null)
            throw new Exception("main form not registration");
    }
}
Теперь меняем инициализацию приложения на старте: нам нужно до отображения формы зарегестрировать её в UIManager
C#
1
2
3
4
ApplicationConfiguration.Initialize();
var mainForm = new MainForm();
UIManager.Instance.RegMainForm(mainForm);
Application.Run(mainForm);
Далее -- добавляем логику переключения на самих котролах
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public partial class AdminUserControl : UserControl
{
    public AdminUserControl() { InitializeComponent(); }
 
    private void ButtonClick(object sender, EventArgs e)
        => UIManager.Instance.ShowClientUserControl();
}
 
public partial class ClientUserControl : UserControl
{
    public ClientUserControl() { InitializeComponent(); }
 
    private void ButtonClick(object sender, EventArgs e)
        => UIManager.Instance.ShowAdminUserControl();
}
Билдим проект. Теперь у нас в toolbox появились наши UserControl. Открываем дизайн главной формы и размещаем на ней один из контролов. Запускаем и проверяем что все работает.
Нажмите на изображение для увеличения
Название: toolbox.jpg
Просмотров: 230
Размер:	41.7 Кб
ID:	8420

Если глянуть использование ОЗУ при очень частых переключениях, то можно заметить что память всё равно растет. Связано это с тем что GC не вызывается. И его принудительное пинание не особо меняет погоду. Причина в том что при наличии свободных 10+Гб clr не видит смысла лишний раз прелопачивать ОЗУ. Лично на моем ноуте я мог добить ОЗУ до целых 2ГБ и только тогда память начинала переиспользоваться.

Окей, в целом этого примера уже достаточно для освоения и разработки условного singel-form приложения. Правда прям в таком виде использовать будет не удобно по многим причинам:
  • нет "выравнивания" размера окна, если контрол имеет каки-либо ограничения
  • "закрытие" старого окна может сопровождаться определеными проверками (например мы в разделе редактирования и у нас есть не сохраненные данные). Это стоит учитывать.
  • класс навигации также знает про создание компонентов. По хорошему это нужно вынести в отдельную фабрику.
  • не плохо бы иметь возможность задавать заголовок окна относительно содержимого.

Окей, сделаем новый более сложный пример и дополним этими фичами.
полное ТЗ
Сделаем следующее приложение:
  • на старте показываем окно авторизации
  • далее мы показываем список неких Items, которые вычитываются из mock данных в ОЗУ
  • каждый Item имеет кнопку "редактировать", которая перекидывает на форму редактирования конкретной записи
  • также на главной форме со списком есть кнопка "добавить", которая позволяет открыть форму добавления (по сути таже форма, что и для редактирования, только без заполненных полей)


Для начала введем не обязательный интерфейс для контролов
C#
1
2
3
4
5
6
internal interface IViewUserControl
{
    public string? Title { get; }
    public bool Closing();
    public bool Close();
}
Title позволит указать в заголовке окна текст, Closing -- выполнить проверку перед переключением или закрытием. Насчет метода Close -- он 100% должен быть, но есть подозрение что всё таки в качестве void при отработке Closing.

Далее -- главное окно. Его создаем как отдельный класс, не через дизайнер.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
internal class MainForm : Form
{
    private Control? viewControl;
 
    public Control? View
    {
        get => viewControl;
        set
        {
            viewControl = value;
            Controls.Remove(viewControl);
            Controls.Add(value);
        }
    }
 
    protected override void OnClosing(CancelEventArgs e)
    {
        base.OnClosing(e);
        if (viewControl is IViewUserControl view)
            e.Cancel = !(view.Closing() && view.Close());
    }
}
Тут всё просто: можем задать текущий внутренний контрол (учитывая ситуацию, когда главное окно должно содержать какое-то количество других контролов. Например общего меню. По сути это аналог Layout). Также переопределяем закрытие: предварительно спрашиваем внутренний контрол на возможность закрыть.

Теперь займемся внутренней логикой приложения: объявим класс отвечающий за текущую авторизацию пользователя
C#
1
2
3
4
5
6
7
internal record User(int Id, string Login);
 
internal class UserContext
{
    public User? CurrentUser { get; set; }
    public UserContext() { }
}
Добавим сервис с mock данными. В нем предусмотрим возможность вычитки, обновления и удаления Items. Так же он будет содержать проверку логин-пароль. (По сути мы эмулируем БД)
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
internal record UserData(int Id, string Login, string Password);
 
internal record DataItem(
    int Id,
    string Title,
    DateTime Date,
    string Text,
    byte[]? Image,
    UserData Author);
 
internal class DataService
{
    private static readonly UserData[] _usersMock;
    private static readonly List<DataItem> _dataMock;
    private readonly UserContext _userContext;
 
    private int AuthorId => _userContext.CurrentUser!.Id;
 
    static DataService()
    {
        var index = 1;
        var admin = new UserData(index++, "admin", "Nya!");
        var client = new UserData(index++, "client", "123");
        _usersMock = [admin, client];
 
        index = 0;
        _dataMock =
        [
            new DataItem(index++,
                "Hatsune Miku (初音ミク)",
                new DateTime(2007, 8, 31),
                @"Hatsune Miku (Japanese: 初音ミク), also...",
                null,
                admin),
            new DataItem(index++,
                "Kagamine Rin (鏡音リン)",
                new DateTime(2007, 12, 27),
                @"Kagamine Rin(Japanese: 鏡音リン), officially...",
                null,
                client),
            new DataItem(index++,
                "Kagamine Len (鏡音レン)",
                new DateTime(2007, 12, 27),
                @"Kagamine Len(Japanese: 鏡音リン), officially...",
                null,
                client),
            new DataItem(index++,
                "Megurine Luka (巡音ルカ)",
                new DateTime(2009, 1, 30),
                @"Megurine Luka (巡音ルカ, Megurine Ruka), codenamed CV03...",
                null,
                admin)
        ];
    }
    public DataService(UserContext userContext)
    {
        _userContext = userContext;
    }
 
    public User? SignIn(string login, string password)
    {
        var user = _usersMock
            .FirstOrDefault(x => 
                x.Login.Equals(login, StringComparison.OrdinalIgnoreCase)
                && x.Password == password);
        return user is null
            ? default
            : new User(user.Id, user.Login);
    }
    public IEnumerable<DataItem> LoadData(string filter)
    {
        var query = _dataMock.Where(x => x.Author.Id == AuthorId);
        if (!string.IsNullOrWhiteSpace(filter))
            query = query.Where(x => x.Title.Contains(filter)
                                  || x.Text.Contains(filter));
        return query.ToArray();
    }
    public DataItem SaveData(int? id, string title, DateTime date, string text, byte[]? image)
    {
        var author = _usersMock.First(x => x.Id == AuthorId);
        if (id.HasValue)
        {
            var index = _dataMock.FindIndex(x => x.Id == id);
            return _dataMock[index] = new DataItem(id.Value, title, date, text, image, author);
        }
        else
        {
            var nextId = _dataMock.Max(x => x.Id) + 1;
            var dataItem = new DataItem(nextId, title, date, text, image, author);
            _dataMock.Add(dataItem);
            return dataItem;
        }
    }
    public void DeleteData(int id)
        => _dataMock.RemoveAll(x => x.Id == id && x.Author.Id == AuthorId);
}
Ещё один сервис, выполняющий "упаковку" изображения в разрешение 200х200. Этот сервис исключительно для визуала + далее показать в фабрике определенный момент по тому как можно такие не глобальные сервисы создавать только по необходимости.
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
internal class ImageService
{
    public byte[] Conversion(string path)
    {
        using var image = Image.FromFile(path);
        using var bitmap = new Bitmap(200, 200);
        using var graphic = Graphics.FromImage(bitmap);
 
        int w = bitmap.Width, h = bitmap.Height, x = 0, y = 0;
 
        if (image.Width > image.Height)
        {
            var k = image.Height / (double)h;
            w = Convert.ToInt32(Math.Round(image.Width / k));
            x = -(w - bitmap.Width) / 2;
        }
        else
        {
            var k = image.Width / (double)w;
            h = Convert.ToInt32(Math.Round(image.Height / k));
            y = -(h - bitmap.Height) / 2;
        }
 
        graphic.DrawImage(image, x, y, w, h);
 
        using var memory = new MemoryStream();
        bitmap.Save(memory, ImageFormat.Jpeg);
        return memory.ToArray();
    }
}
Теперь добавляем наши контролы. Пока у нас не готова навигация, помечаю места для вызова перехода на другую страницу как TODO: %name control%.

Авторизация. Сильно не мудрю, поэтому прям в форме вычитка из БД через сервис и прописывание в контекст.
Нажмите на изображение для увеличения
Название: auth.jpg
Просмотров: 201
Размер:	38.3 Кб
ID:	8421
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
internal partial class AuthUserControl : UserControl
{
    private readonly UserContext _userContext;
    private readonly DataService _dataService;
 
    public AuthUserControl(
        UserContext userContext,
        DataService dataService)
    {
        InitializeComponent();
        _userContext = userContext;
        _dataService = dataService;
    }
 
    private void SignInButtonClick(object sender, EventArgs e)
    {
        if (!IsValid())
            return;
 
        var login = loginTextBox.Text;
        var password = passwordTextBox.Text;
        var user = _dataService.SignIn(login, password);
        if (user == null)
        {
            errorLabel.Visible = true;
        }
        else
        {
            _userContext.CurrentUser = user;
            // TODO: on main
        }
    }
    private bool IsValid()
        => !string.IsNullOrEmpty(loginTextBox.Text)
        && !string.IsNullOrEmpty(passwordTextBox.Text);
}
Создадим контрол для отбражения отдельной записи. На контроле будет две функциональные кнопки: "редактировать" и "удалить". По их нажатию мы просто будем пинать ивенты, а обработку вынесем наружу. В самом конструкторе зададим установку отображения значений входящей записи (текст, изображение и прочее).
Нажмите на изображение для увеличения
Название: item.jpg
Просмотров: 193
Размер:	20.2 Кб
ID:	8423
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
internal partial class DataItemUserControl : UserControl
{
    private readonly DataItem _dataItem;
 
    public event Action<DataItemUserControl, DataItem>? OnDataDelete;
    public event Action<DataItemUserControl, DataItem>? OnDataEdit;
 
    public DataItemUserControl(DataItem dataItem)
    {
        InitializeComponent();
        _dataItem = dataItem;
        titleLabel.Text = dataItem.Title;
        dateLabel.Text = $"{dataItem.Date:dd MMMM yyyy}";
        richTextBox1.Text = dataItem.Text;
        this.SetImage(imagePictureBox, dataItem.Image);
    }
 
    private void EditButtonClick(object sender, EventArgs e)
        => OnDataEdit?.Invoke(this, _dataItem);
    private void DeleteButtonClick(object sender, EventArgs e)
    {
        if (this.ShowConfirmation("Delete item?"))
            OnDataDelete?.Invoke(this, _dataItem);
    }
}
Основное представление. Тут просто список в виде FlowLayoutPanel, а также кнопка "добавить". Для наглядности добавил отображение текущего профиля и строку для фильтрации наших записей. LoadData - загрузка данных из "БД" и закидываем в виде юзер-контролов на панель. Так как контролы могут только информировать через ивенты об попытке редактирования/удаления, не забываем подвязать обработчики (в идеале эту логику стоит вынести в сами DataItemUserControl, но без подвязки данных очень трудно пропихнуть удаление на главное представление, а также их тоже придется запихнуть в фабрику. Мне лень было дополнительно тратить время на продумывание всего этого, потому оставил так). Также реализуем интерфейс IViewUserControl, чтобы можно было задать заголовок окну.
Нажмите на изображение для увеличения
Название: main.jpg
Просмотров: 207
Размер:	23.2 Кб
ID:	8422
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
internal partial class MainUserControl : UserControl, IViewUserControl
{
    private readonly UserContext _userContext;
    private readonly DataService _dataService;
 
    public string? Title => "client main section";
    private int WidhtDataItemControl => flowLayoutPanel.ClientSize.Width - 25;
 
    public MainUserControl(
        UserContext userContext,
        DataService dataService)
    {
        InitializeComponent();
        _userContext = userContext;
        _dataService = dataService;
        _uiNavigation = uiNavigation;
    }
 
    public bool Close() => true;
    public bool Closing() => true;
 
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        userTitleLabel.Text = $"Hello, {_userContext.CurrentUser!.Login}!";
        LoadData();
    }
 
    private void SearchButtonClick(object sender, EventArgs e)
        => LoadData();
    private void AddButtonClick(object sender, EventArgs e)
    { /*TODO: on add*/ }
    private void OnDataEdit(DataItemUserControl _, DataItem dataItem)
    { /*TODO: on edit*/ }
    private void OnDataDelete(DataItemUserControl control, DataItem dataItem)
    {
        _dataService.DeleteData(dataItem.Id);
        flowLayoutPanel.Controls.Remove(control);
        control.Dispose();
    }
 
    private void LoadData()
    {
        var filter = filterTextBox.Text;
        Clear(flowLayoutPanel);
        var data = _dataService.LoadData(filter);
 
        var editMethod = OnDataEdit;
        var deleteMethod = OnDataDelete;
 
        foreach (var item in data)
        {
            var control = new DataItemUserControl(item)
            {
                Width = WidhtDataItemControl
            };
            control.OnDataEdit += editMethod;
            control.OnDataDelete += deleteMethod;
            flowLayoutPanel.Controls.Add(control);
            control.Disposed += (_, __) =>
            {
                control.OnDataEdit -= editMethod;
                control.OnDataDelete -= deleteMethod;
            };
        }
    }
    private void Clear(Control control)
    {
        var olds = new Control[control.Controls.Count];
        control.Controls.CopyTo(olds, 0);
        control.Controls.Clear();
        foreach (IDisposable child in olds)
            child.Dispose();
    }
}
Форма (точнее контрол) редактирования. Также реализуем IViewUserControl, но в нашем случае главное метод Closing -- нужно выполнить проверку, что данные (не) были изменены, и соответственно вызвать уведомление. В остальном всё просто: распихиваем данные (при наличии) по нужным контролам, подвязываем обработку "сохранить" и "назад". Валидация и прочие проверки по вкусу.
Нажмите на изображение для увеличения
Название: edit.jpg
Просмотров: 212
Размер:	38.5 Кб
ID:	8424
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
internal partial class EditDataItemUserControl : UserControl, IViewUserControl
{
    private readonly DataService _dataService;
    private readonly ImageService _imageService;
    private DataItem? originalDataItem;
    private bool chageImage;
 
    public string? Title { get; }
 
    public EditDataItemUserControl(
        DataItem? dataItem, 
        DataService dataService,
        ImageService imageService)
    {
        InitializeComponent();
        _dataService = dataService;
        _imageService = imageService;
        SetData(dataItem);
        Title = dataItem is null
            ? "New record"
            : $"Edit record: {dataItem.Title}";
    }
 
    public bool Close() => true;
    public bool Closing()
    {
        if (HaveChanges())
        {
            if (originalDataItem == null && AllEmptry())
                return true;
 
            switch (this.ShowWarning("Save changes?"))
            {
                case DialogResult.Yes:
                    if (IsValid())
                    {
                        Save();
                        return true;
                    }
                    else
                    { 
                        this.ShowAlert("Title can't be emptry!", "not valid data", MessageBoxIcon.Warning);
                        return false;
                    }
                case DialogResult.No:
                    return true;
                default:
                    return false;
            }
        }
        return true;
    }
 
    protected override void OnSizeChanged(EventArgs e)
    {
        base.OnSizeChanged(e);
        this.AlignByCenter(centralPanel, backgroundImage);
    }
 
    private void EditPictureBoxClick(object sender, EventArgs e)
    {
        var dialog = new OpenFileDialog
        {
            Multiselect = false,
            Filter = "image|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tif;*.tiff;*.webp"
        };
        if (dialog.ShowDialog(this) == DialogResult.OK)
        {
            try
            {
                var raw = _imageService.Conversion(dialog.FileName);
                this.SetImage(imagePictureBox, raw);
                chageImage = true;
            }
            catch
            {
                this.ShowError("Can't open image or parse image");
            }
        }
    }
    private void SaveButtonClick(object sender, EventArgs e)
    {
        if (IsValid())
            Save(true);
        else
            this.ShowAlert("Title can't be emptry!", "not valid data", MessageBoxIcon.Warning);
    }
    private void BackButtonClick(object sender, EventArgs e)
    { /*TODO: on main*/ }
 
    private void SetData(DataItem? dataItem)
    {
        originalDataItem = dataItem;
        titleTextBox.Text = dataItem?.Title ?? string.Empty;
        dateTimePicker.Value = dataItem?.Date ?? DateTime.Now;
        richTextBox.Text = dataItem?.Text ?? string.Empty;
        this.SetImage(imagePictureBox, dataItem?.Image);
    }
    private void Save(bool notifier = false)
    {
        var id = originalDataItem?.Id;
        var title = titleTextBox.Text;
        var text = richTextBox.Text;
        var date = dateTimePicker.Value;
        var image = (imagePictureBox.DataContext as MemoryStream)?.ToArray();
        originalDataItem = _dataService.SaveData(id, title, date, text, image);
        chageImage = false;
        if (notifier)
            this.ShowAlert("Completed");
    }
    private bool HaveChanges()
        => chageImage
        || originalDataItem is null
        || originalDataItem.Title != titleTextBox.Text
        || originalDataItem.Text != richTextBox.Text
        || originalDataItem.Date != dateTimePicker.Value;
    private bool IsValid()
        => !string.IsNullOrWhiteSpace(titleTextBox.Text);
    private bool AllEmptry()
        => string.IsNullOrWhiteSpace(titleTextBox.Text)
        && string.IsNullOrWhiteSpace(richTextBox.Text)
        && imagePictureBox.Image is null;
}
Фабрика. Как можно заметить -- всё написано с подходом Dependency Injection. Можно и без него, но это более "взрослый" подход, о котором нужно читать отдельную лекцию (не в данном разборе). Просто смиритесь что "так надо". По всему приложение есть два основных сервиса, которые используются постоянно: UserContext и DataService (далее ещё добавится UINavigation). Зашьем прям в нашей фабрике эти инстанцы, причем ленивой инициализацией. И надобавляем методов инициализации наших UI компонентов.

Предположим что редактирование записей происходит не часто, поэтому в инициализации EditDataItemUserControl сервис ImageService будет создавать каждый раз по новой.
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
internal class UIFactory
{
    private readonly Lazy<UserContext> _globalUserContext;
    private readonly Lazy<DataService> _globalDataService;
    private readonly Lazy<MainForm> _mainForm;
 
    public UIFactory()
    {
        _globalUserContext = new(() => new());
        _globalDataService = new(() => new(_globalUserContext.Value));
        _mainForm = new(() =>
        {
            var mainForm = new MainForm
            {
                Size = new Size(1400, 720),
                StartPosition = FormStartPosition.CenterScreen,
                Icon = Resources.app
            };
            _globalUINavigation.Value.RegistrationMainForm(mainForm);
            return mainForm;
        });
    }
 
    public MainForm MainForm()
        => _mainForm.Value;
    public AuthUserControl AuthUserControl()
        => new(_globalUserContext.Value, _globalDataService.Valu);
    public MainUserControl MainUserControl()
        => new(_globalUserContext.Value, _globalDataService.Value);
    public EditDataItemUserControl EditUserControl(DataItem? dataItem)
        => new(dataItem, _globalDataService.Value, new());
}
Навигация. Подход прост (смотрим метод SwitchUserControl):
  • инициализируем через фабрику нужный контрол
  • если старый контрол реализует IViewUserControl -- пинаем Closing и проверяем что отработало.
  • заменяем контрол на MainForm, старый -- Dispose
  • опционально выравниваем размер окна по минимальным значениям и проставляем заголовок
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
internal class UINavigation(UIFactory uiFactory)
{
    private readonly UIFactory _uiFactory = uiFactory;
    private MainForm mainForm = default!;
 
    public void RegistrationMainForm(MainForm form)
        => mainForm = form;
    public void OnMain()
        => SwitchUserControl(_uiFactory.MainUserControl);
    public void OnAuthorization()
        => SwitchUserControl(_uiFactory.AuthUserControl);
    public void OnEdit(DataItem? dataItem = null)
        => SwitchUserControl(() => _uiFactory.EditUserControl(dataItem));
 
    private void CheckRegMainForm()
    {
        if (mainForm is null)
            throw new Exception("Main form not registration!");
    }
    private void SwitchUserControl(Func<UserControl> userControlFactory)
    {
        CheckRegMainForm();
        if (CloseCurrentUserControl())
        { 
            var userControl = userControlFactory();
            var old = mainForm.View;
            userControl.Dock = DockStyle.Fill;
            var delta = mainForm.Size - mainForm.ClientSize;
            mainForm.ClientSize = new Size(
                Math.Max(userControl.MinimumSize.Width, mainForm.ClientSize.Width),
                Math.Max(userControl.MinimumSize.Height, mainForm.ClientSize.Height));
            mainForm.MinimumSize = userControl.MinimumSize + delta;
            mainForm.Text = (userControl as IViewUserControl)?.Title ?? string.Empty;
            mainForm.View = userControl;
            old?.Dispose();
        }
    }
    private bool CloseCurrentUserControl()
        => mainForm.View is IViewUserControl view
            ? view.Closing() && view.Close()
            : true;
}
Далее добавляем в саму фабрику также инициализацию и для UINavigation (может немного хрустнуть в мозге от этой рекурсии в конструкторах, но изначальная у нас именно фабрика).
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal class UIFactory
{
    //... other fields
    private readonly Lazy<UINavigation> _globalUINavigation;
 
    public UIFactory()
    {
        //... 
        _globalUINavigation = new(() => new(this));
    }
 
    public UINavigation UINavigation()
        => _globalUINavigation.Value;
}
Распихиваем навигацию по конструкторам UI элементов и заменяем TODO: %name control% на нужные методы. Остается только подправить старт всего приложения, и готово.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
internal static class Program
{
    [STAThread]
    public static void Main()
    {
        ApplicationConfiguration.Initialize();
        var mainForm = Init();
        Application.Run(mainForm);
    }
 
    private static MainForm Init()
    {
        var factory = new UIFactory();
        var uiNavigation = factory.UINavigation();
        var mainForm = factory.MainForm();
        uiNavigation.OnAuthorization();
        return mainForm;
    }
}
Финальный код прилагается Nya.UserControlNavigationExample.zip
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 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