Form1.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. using System.Data;
  2. using Npgsql;
  3. namespace praktika
  4. {
  5. public partial class Form1 : Form
  6. {
  7. public const string SchemaName = "dyomin_be";
  8. public const string AdminLogin = "admin";
  9. public const string AdminPassword = "admin";
  10. public const string ConnectionString =
  11. "Host=edu-pg.itiscaf.ru;Port=5432;Database=dbwork;Username=dyomin_be;Password=$vAx&y5zxQ$;Search Path=dyomin_be";
  12. public readonly TextBox loginTextBox = new();
  13. public readonly TextBox passwordTextBox = new();
  14. public readonly Button loginButton = new();
  15. public readonly Label loginStatusLabel = new();
  16. public readonly Panel loginPanel = new();
  17. public readonly Panel mainPanel = new();
  18. public readonly ComboBox tableComboBox = new();
  19. public readonly DataGridView dataGridView = new();
  20. public readonly Button refreshButton = new();
  21. public readonly Button addButton = new();
  22. public readonly Button saveButton = new();
  23. public readonly Button deleteButton = new();
  24. public readonly Button logoutButton = new();
  25. public readonly Label currentUserLabel = new();
  26. public readonly Dictionary<string, TableInfo> tables = new()
  27. {
  28. ["authors"] = new("id"),
  29. ["books"] = new("id"),
  30. ["tags"] = new("id"),
  31. ["book_tags"] = new("book_id", "tag_id") { AutoIncrementKeys = [] },
  32. ["comments"] = new("id"),
  33. ["users"] = new("id") { AdminOnly = true },
  34. ["v_book_catalog"] = new() { ReadOnly = true }
  35. };
  36. public DataTable? currentTable;
  37. public bool isAdmin;
  38. public string currentLogin = "";
  39. public Form1()
  40. {
  41. InitializeComponent();
  42. BuildUi();
  43. ShowLogin();
  44. }
  45. public void BuildUi()
  46. {
  47. Text = "PostgreSQL CRUD";
  48. Width = 1100;
  49. Height = 700;
  50. StartPosition = FormStartPosition.CenterScreen;
  51. MinimumSize = new Size(900, 550);
  52. loginPanel.Dock = DockStyle.Fill;
  53. var loginBox = new TableLayoutPanel
  54. {
  55. Width = 360,
  56. Height = 210,
  57. Anchor = AnchorStyles.None,
  58. ColumnCount = 1,
  59. RowCount = 7,
  60. Padding = new Padding(20)
  61. };
  62. loginBox.Location = new Point((loginPanel.Width - loginBox.Width) / 2, (loginPanel.Height - loginBox.Height) / 2);
  63. loginBox.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
  64. loginBox.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
  65. loginBox.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
  66. loginBox.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
  67. loginBox.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
  68. loginBox.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
  69. loginBox.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
  70. loginBox.Controls.Add(new Label { Text = "Логин", Dock = DockStyle.Fill }, 0, 0);
  71. loginTextBox.Dock = DockStyle.Fill;
  72. loginBox.Controls.Add(loginTextBox, 0, 1);
  73. loginBox.Controls.Add(new Label { Text = "Пароль", Dock = DockStyle.Fill }, 0, 2);
  74. passwordTextBox.Dock = DockStyle.Fill;
  75. passwordTextBox.UseSystemPasswordChar = true;
  76. loginBox.Controls.Add(passwordTextBox, 0, 3);
  77. loginButton.Text = "Войти";
  78. loginButton.Dock = DockStyle.Fill;
  79. loginButton.Click += LoginButton_Click;
  80. loginBox.Controls.Add(loginButton, 0, 4);
  81. loginBox.Controls.Add(new Label { Text = "Админ: admin / admin", Dock = DockStyle.Fill }, 0, 5);
  82. loginStatusLabel.ForeColor = Color.DarkRed;
  83. loginStatusLabel.Dock = DockStyle.Fill;
  84. loginBox.Controls.Add(loginStatusLabel, 0, 6);
  85. loginPanel.Controls.Add(loginBox);
  86. Controls.Add(loginPanel);
  87. mainPanel.Dock = DockStyle.Fill;
  88. mainPanel.Visible = false;
  89. var root = new TableLayoutPanel
  90. {
  91. Dock = DockStyle.Fill,
  92. ColumnCount = 1,
  93. RowCount = 2,
  94. Padding = new Padding(10)
  95. };
  96. root.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
  97. root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
  98. var toolbar = new FlowLayoutPanel
  99. {
  100. Dock = DockStyle.Fill,
  101. FlowDirection = FlowDirection.LeftToRight,
  102. WrapContents = false
  103. };
  104. currentUserLabel.AutoSize = true;
  105. currentUserLabel.Padding = new Padding(0, 8, 16, 0);
  106. toolbar.Controls.Add(currentUserLabel);
  107. tableComboBox.Width = 180;
  108. tableComboBox.DropDownStyle = ComboBoxStyle.DropDownList;
  109. tableComboBox.SelectedIndexChanged += TableComboBox_SelectedIndexChanged;
  110. toolbar.Controls.Add(tableComboBox);
  111. refreshButton.Text = "Обновить";
  112. refreshButton.Click += RefreshButton_Click;
  113. toolbar.Controls.Add(refreshButton);
  114. addButton.Text = "Добавить";
  115. addButton.Click += AddButton_Click;
  116. toolbar.Controls.Add(addButton);
  117. saveButton.Text = "Сохранить";
  118. saveButton.Click += SaveButton_Click;
  119. toolbar.Controls.Add(saveButton);
  120. deleteButton.Text = "Удалить";
  121. deleteButton.Click += DeleteButton_Click;
  122. toolbar.Controls.Add(deleteButton);
  123. logoutButton.Text = "Выйти";
  124. logoutButton.Click += LogoutButton_Click;
  125. toolbar.Controls.Add(logoutButton);
  126. dataGridView.Dock = DockStyle.Fill;
  127. dataGridView.AllowUserToAddRows = false;
  128. dataGridView.AllowUserToDeleteRows = false;
  129. dataGridView.AutoGenerateColumns = true;
  130. dataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
  131. dataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
  132. dataGridView.MultiSelect = true;
  133. root.Controls.Add(toolbar, 0, 0);
  134. root.Controls.Add(dataGridView, 0, 1);
  135. mainPanel.Controls.Add(root);
  136. Controls.Add(mainPanel);
  137. AcceptButton = loginButton;
  138. }
  139. public void LoginButton_Click(object? sender, EventArgs e)
  140. {
  141. loginStatusLabel.Text = "";
  142. var login = loginTextBox.Text.Trim();
  143. var password = passwordTextBox.Text;
  144. if (login == AdminLogin && password == AdminPassword)
  145. {
  146. isAdmin = true;
  147. currentLogin = login;
  148. OpenMain();
  149. return;
  150. }
  151. try
  152. {
  153. using var connection = new NpgsqlConnection(ConnectionString);
  154. connection.Open();
  155. using var command = new NpgsqlCommand(
  156. $"select count(*) from {Q(SchemaName)}.{Q("users")} " +
  157. $"where login = @login and password_hash = crypt(@password, password_hash)",
  158. connection);
  159. command.Parameters.AddWithValue("login", login);
  160. command.Parameters.AddWithValue("password", password);
  161. var exists = Convert.ToInt32(command.ExecuteScalar()) > 0;
  162. if (!exists)
  163. {
  164. loginStatusLabel.Text = "Неверный логин или пароль";
  165. return;
  166. }
  167. isAdmin = false;
  168. currentLogin = login;
  169. OpenMain();
  170. }
  171. catch (Exception ex)
  172. {
  173. loginStatusLabel.Text = ex.Message;
  174. }
  175. }
  176. public void OpenMain()
  177. {
  178. loginPanel.Visible = false;
  179. mainPanel.Visible = true;
  180. currentUserLabel.Text = isAdmin ? $"Админ: {currentLogin}" : $"Пользователь: {currentLogin}";
  181. AcceptButton = saveButton;
  182. tableComboBox.Items.Clear();
  183. foreach (var table in tables.Where(x => isAdmin || !x.Value.AdminOnly).Select(x => x.Key))
  184. {
  185. tableComboBox.Items.Add(table);
  186. }
  187. if (tableComboBox.Items.Count > 0)
  188. {
  189. tableComboBox.SelectedIndex = 0;
  190. }
  191. }
  192. public void ShowLogin()
  193. {
  194. mainPanel.Visible = false;
  195. loginPanel.Visible = true;
  196. loginTextBox.Text = "";
  197. passwordTextBox.Text = "";
  198. loginStatusLabel.Text = "";
  199. dataGridView.DataSource = null;
  200. currentTable = null;
  201. AcceptButton = loginButton;
  202. loginTextBox.Focus();
  203. }
  204. public void TableComboBox_SelectedIndexChanged(object? sender, EventArgs e)
  205. {
  206. LoadCurrentTable();
  207. }
  208. public void RefreshButton_Click(object? sender, EventArgs e)
  209. {
  210. LoadCurrentTable();
  211. }
  212. public void AddButton_Click(object? sender, EventArgs e)
  213. {
  214. if (currentTable is null || SelectedTable().ReadOnly)
  215. {
  216. return;
  217. }
  218. currentTable.Rows.Add(currentTable.NewRow());
  219. }
  220. public void DeleteButton_Click(object? sender, EventArgs e)
  221. {
  222. if (currentTable is null || SelectedTable().ReadOnly)
  223. {
  224. return;
  225. }
  226. foreach (DataGridViewRow gridRow in dataGridView.SelectedRows)
  227. {
  228. if (gridRow.DataBoundItem is DataRowView rowView)
  229. {
  230. rowView.Row.Delete();
  231. }
  232. }
  233. }
  234. public void SaveButton_Click(object? sender, EventArgs e)
  235. {
  236. if (currentTable is null)
  237. {
  238. return;
  239. }
  240. dataGridView.EndEdit();
  241. if (BindingContext is { } context && context[currentTable] is { } manager)
  242. {
  243. manager.EndCurrentEdit();
  244. }
  245. try
  246. {
  247. var tableName = SelectedTableName();
  248. var table = SelectedTable();
  249. if (table.ReadOnly)
  250. {
  251. throw new InvalidOperationException("Эту таблицу нельзя редактировать");
  252. }
  253. using var connection = new NpgsqlConnection(ConnectionString);
  254. connection.Open();
  255. using var transaction = connection.BeginTransaction();
  256. foreach (DataRow row in currentTable.Rows.Cast<DataRow>().Where(x => x.RowState == DataRowState.Deleted).ToList())
  257. {
  258. DeleteRow(connection, transaction, tableName, table, row);
  259. }
  260. foreach (DataRow row in currentTable.Rows.Cast<DataRow>().Where(x => x.RowState == DataRowState.Added).ToList())
  261. {
  262. InsertRow(connection, transaction, tableName, table, row);
  263. }
  264. foreach (DataRow row in currentTable.Rows.Cast<DataRow>().Where(x => x.RowState == DataRowState.Modified).ToList())
  265. {
  266. UpdateRow(connection, transaction, tableName, table, row);
  267. }
  268. transaction.Commit();
  269. LoadCurrentTable();
  270. MessageBox.Show("Сохранено", "Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
  271. }
  272. catch (Exception ex)
  273. {
  274. MessageBox.Show(ex.Message, "Ошибка сохранения", MessageBoxButtons.OK, MessageBoxIcon.Error);
  275. }
  276. }
  277. public void LogoutButton_Click(object? sender, EventArgs e)
  278. {
  279. isAdmin = false;
  280. currentLogin = "";
  281. tableComboBox.Items.Clear();
  282. ShowLogin();
  283. }
  284. public void LoadCurrentTable()
  285. {
  286. if (tableComboBox.SelectedItem is not string tableName)
  287. {
  288. return;
  289. }
  290. try
  291. {
  292. var table = tables[tableName];
  293. var orderBy = table.PrimaryKeys.Count > 0
  294. ? " order by " + string.Join(", ", table.PrimaryKeys.Select(Q))
  295. : "";
  296. using var connection = new NpgsqlConnection(ConnectionString);
  297. connection.Open();
  298. using var command = new NpgsqlCommand($"select * from {Q(SchemaName)}.{Q(tableName)}{orderBy}", connection);
  299. using var reader = command.ExecuteReader();
  300. currentTable = new DataTable(tableName);
  301. currentTable.Load(reader);
  302. PrepareTable(tableName, currentTable);
  303. dataGridView.DataSource = currentTable;
  304. dataGridView.ReadOnly = table.ReadOnly;
  305. addButton.Enabled = !table.ReadOnly;
  306. saveButton.Enabled = !table.ReadOnly;
  307. deleteButton.Enabled = !table.ReadOnly;
  308. }
  309. catch (Exception ex)
  310. {
  311. MessageBox.Show(ex.Message, "Ошибка загрузки", MessageBoxButtons.OK, MessageBoxIcon.Error);
  312. }
  313. }
  314. public void PrepareTable(string tableName, DataTable dataTable)
  315. {
  316. var table = tables[tableName];
  317. foreach (DataColumn column in dataTable.Columns)
  318. {
  319. column.ReadOnly = false;
  320. column.AllowDBNull = true;
  321. }
  322. foreach (var key in table.PrimaryKeys)
  323. {
  324. if (dataTable.Columns.Contains(key))
  325. {
  326. dataTable.Columns[key]!.ReadOnly = table.AutoIncrementKeys.Contains(key);
  327. }
  328. }
  329. }
  330. public void InsertRow(NpgsqlConnection connection, NpgsqlTransaction transaction, string tableName, TableInfo table, DataRow row)
  331. {
  332. var columns = currentTable!.Columns
  333. .Cast<DataColumn>()
  334. .Select(x => x.ColumnName)
  335. .Where(x => !table.AutoIncrementKeys.Contains(x) && row[x] != DBNull.Value)
  336. .ToList();
  337. if (columns.Count == 0)
  338. {
  339. throw new InvalidOperationException("Нет данных для добавления");
  340. }
  341. var columnList = string.Join(", ", columns.Select(Q));
  342. var valueList = string.Join(", ", columns.Select((_, index) => $"@p{index}"));
  343. using var command = new NpgsqlCommand(
  344. $"insert into {Q(SchemaName)}.{Q(tableName)} ({columnList}) values ({valueList})",
  345. connection,
  346. transaction);
  347. AddParameters(command, columns, row, DataRowVersion.Current, 0);
  348. command.ExecuteNonQuery();
  349. }
  350. public void UpdateRow(NpgsqlConnection connection, NpgsqlTransaction transaction, string tableName, TableInfo table, DataRow row)
  351. {
  352. var columns = currentTable!.Columns
  353. .Cast<DataColumn>()
  354. .Select(x => x.ColumnName)
  355. .Where(x => !table.PrimaryKeys.Contains(x))
  356. .ToList();
  357. if (columns.Count == 0)
  358. {
  359. return;
  360. }
  361. var setSql = string.Join(", ", columns.Select((column, index) => $"{Q(column)} = @p{index}"));
  362. var whereSql = BuildWhere(table, row, DataRowVersion.Original, columns.Count);
  363. using var command = new NpgsqlCommand(
  364. $"update {Q(SchemaName)}.{Q(tableName)} set {setSql} where {whereSql}",
  365. connection,
  366. transaction);
  367. AddParameters(command, columns, row, DataRowVersion.Current, 0);
  368. AddParameters(command, table.PrimaryKeys, row, DataRowVersion.Original, columns.Count);
  369. command.ExecuteNonQuery();
  370. }
  371. public void DeleteRow(NpgsqlConnection connection, NpgsqlTransaction transaction, string tableName, TableInfo table, DataRow row)
  372. {
  373. var whereSql = BuildWhere(table, row, DataRowVersion.Original, 0);
  374. using var command = new NpgsqlCommand(
  375. $"delete from {Q(SchemaName)}.{Q(tableName)} where {whereSql}",
  376. connection,
  377. transaction);
  378. AddParameters(command, table.PrimaryKeys, row, DataRowVersion.Original, 0);
  379. command.ExecuteNonQuery();
  380. }
  381. public static string BuildWhere(TableInfo table, DataRow row, DataRowVersion version, int offset)
  382. {
  383. if (table.PrimaryKeys.Count == 0)
  384. {
  385. throw new InvalidOperationException("Для CRUD нужен первичный ключ");
  386. }
  387. return string.Join(" and ", table.PrimaryKeys.Select((key, index) => $"{Q(key)} = @p{offset + index}"));
  388. }
  389. public static void AddParameters(NpgsqlCommand command, List<string> columns, DataRow row, DataRowVersion version, int offset)
  390. {
  391. for (var i = 0; i < columns.Count; i++)
  392. {
  393. var value = row[columns[i], version];
  394. command.Parameters.AddWithValue($"p{offset + i}", value is DBNull ? DBNull.Value : value);
  395. }
  396. }
  397. public string SelectedTableName()
  398. {
  399. return tableComboBox.SelectedItem as string
  400. ?? throw new InvalidOperationException("Таблица не выбрана");
  401. }
  402. public TableInfo SelectedTable()
  403. {
  404. return tables[SelectedTableName()];
  405. }
  406. public static string Q(string identifier)
  407. {
  408. return "\"" + identifier.Replace("\"", "\"\"") + "\"";
  409. }
  410. public sealed class TableInfo
  411. {
  412. public TableInfo(params string[] primaryKeys)
  413. {
  414. PrimaryKeys = primaryKeys.ToList();
  415. AutoIncrementKeys = primaryKeys.ToList();
  416. }
  417. public List<string> PrimaryKeys { get; init; }
  418. public List<string> AutoIncrementKeys { get; init; }
  419. public bool AdminOnly { get; init; }
  420. public bool ReadOnly { get; init; }
  421. }
  422. }
  423. }