Bangun Aplikasi MangsIpul
Panduan komprehensif pengembangan aplikasi Desktop Windows Forms dengan arsitektur REST API. Dari persiapan lingkungan, desain UI/UX, hingga implementasi CRUD dan deployment.
๐ฏ Mengapa Visual Studio 2022 Community?
Visual Studio 2022 Community adalah IDE gratis dan full-featured yang menjadi standar industri untuk pengembangan .NET. Keunggulannya:
- Debugger Profesional โ Fitur breakpoint, watch, dan immediate window untuk melacak bug dengan presisi tinggi
- WinForms Designer โ Editor visual drag-and-drop untuk mempercepat desain UI
- IntelliCode โ AI-assisted coding yang memprediksi kode, meningkatkan produktivitas ~30%
- NuGet Package Manager โ Manajemen library terintegrasi (Newtonsoft.Json, RestSharp, dll)
- Live Share โ Kolaborasi real-time untuk pair programming
๐ฅ Prosedur Download & Instalasi
Buka
visualstudio.microsoft.com/downloads menggunakan browser.Temukan "Visual Studio Community 2022", klik Free Download. Gratis untuk individu & open source.
Klik kanan file
vs_community.exe โ Run as Administrator.Centang ".NET desktop development". Akan menginstal .NET SDK, WinForms Designer, MSBuild, dan Debugging tools.
Proses 10โ30 menit, total download ~5โ8 GB. Pastikan koneksi stabil.
VS 2022 muncul di Start Menu ยท Bisa membuat project "Windows Forms App" ยท NuGet tersedia via Tools menu
Hanya menampilkan data dan menerima input user. Tidak boleh ada logika bisnis atau akses database langsung.
Plain C# class yang merepresentasikan struktur data. Contoh: User punya property Id, Username, Email.
Semua logika komunikasi dengan API, manajemen session, dan business rules. Form hanya memanggil method dari sini.
Function utility yang reusable: format tanggal, validasi email, navigasi antar form.
Langkah Membuat Proyek Baru
Install NuGet Packages
# Buka Tools โ NuGet Package Manager โ Package Manager Console Install-Package Newtonsoft.Json -Version 13.0.3 Install-Package RestSharp -Version 110.2.0 Install-Package Microsoft.Extensions.Configuration.Json -Version 7.0.0
Program.cs (Entry Point)
using System;
using System.Windows.Forms;
namespace MangsIpulApp
{
internal static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// Form login tampil pertama
Application.Run(new LoginForm());
}
}
}
[STAThread] wajib ada untuk aplikasi WinForms. STA = Single-Threaded Apartment, diperlukan untuk komponen COM seperti Clipboard dan Drag-Drop.Properti Form di Properties Window
txtUsernametxtPassword, UseSystemPasswordChar = True โ WAJIB!btnLogin, BackColor = #6d28d9, ForeColor = White, FlatStyle = Flat, Cursor = Hand๐ก Toggle Show Password
private void chkShowPassword_CheckedChanged(object sender, EventArgs e)
{
// Centang = tampilkan teks; kosong = tampilkan bullet
txtPassword.UseSystemPasswordChar = !chkShowPassword.Checked;
}
1. SessionManager.cs
using System;
namespace MangsIpulApp.Services
{
public static class SessionManager
{
public static string BearerToken { get; set; }
public static string CurrentUserId { get; set; }
public static string CurrentUsername { get; set; }
public static string CurrentUserEmail { get; set; }
public static string CurrentUserRole { get; set; }
public static bool IsLoggedIn => !string.IsNullOrEmpty(BearerToken);
public static void Logout()
{
BearerToken = null;
CurrentUserId = null;
CurrentUsername = null;
CurrentUserEmail = null;
CurrentUserRole = null;
}
}
}
2. ApiService.cs (Singleton HttpClient)
using System;
using System.Net.Http;
using System.Net.Http.Headers;
namespace MangsIpulApp.Services
{
public class ApiService
{
private static HttpClient _client;
private static readonly object _lock = new object();
public static HttpClient Client
{
get
{
lock (_lock)
{
if (_client == null)
{
_client = new HttpClient();
_client.BaseAddress = new Uri("http://localhost:8000/api/");
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
_client.Timeout = TimeSpan.FromSeconds(30);
}
return _client;
}
}
}
public static void SetAuthorizationHeader()
{
if (SessionManager.IsLoggedIn)
Client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", SessionManager.BearerToken);
}
public static void RemoveAuthorizationHeader()
{
Client.DefaultRequestHeaders.Authorization = null;
}
}
}
3. Models/LoginResponse.cs
using Newtonsoft.Json;
namespace MangsIpulApp.Models
{
public class LoginRequest
{
public string email { get; set; }
public string password { get; set; }
}
public class LoginResponse
{
[JsonProperty("token")]
public string Token { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}
public class User
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("role")]
public string Role { get; set; }
}
public class ErrorResponse
{
[JsonProperty("message")]
public string Message { get; set; }
}
}
4. Implementasi Login di LoginForm.cs
private async void btnLogin_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(txtUsername.Text))
{
MessageBox.Show("Username/Email tidak boleh kosong!", "Validasi",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
txtUsername.Focus();
return;
}
if (string.IsNullOrWhiteSpace(txtPassword.Text))
{
MessageBox.Show("Password tidak boleh kosong!", "Validasi",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
txtPassword.Focus();
return;
}
btnLogin.Enabled = false;
btnLogin.Text = "Memproses...";
var loginRequest = new LoginRequest
{
email = txtUsername.Text.Trim(),
password = txtPassword.Text
};
try
{
var json = JsonConvert.SerializeObject(loginRequest);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("login", content);
var responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var result = JsonConvert.DeserializeObject<LoginResponse>(responseBody);
SessionManager.BearerToken = result.Token;
SessionManager.CurrentUserId = result.User.Id.ToString();
SessionManager.CurrentUsername = result.User.Username;
SessionManager.CurrentUserEmail = result.User.Email;
SessionManager.CurrentUserRole = result.User.Role;
ApiService.SetAuthorizationHeader();
MessageBox.Show($"Selamat datang, {result.User.Username}!",
"Login Berhasil", MessageBoxButtons.OK, MessageBoxIcon.Information);
new MainDashboard().Show();
this.Hide();
}
else
{
var err = JsonConvert.DeserializeObject<ErrorResponse>(responseBody);
MessageBox.Show($"Login gagal: {err?.Message ?? "Username atau password salah"}",
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (HttpRequestException ex)
{
MessageBox.Show($"Tidak dapat terhubung ke server.\n{ex.Message}",
"Network Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch (TaskCanceledException)
{
MessageBox.Show("Request timeout. Coba lagi.", "Timeout",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
finally
{
btnLogin.Enabled = true;
btnLogin.Text = "LOGIN";
}
}
๐ Alur Logika Login
Contoh Struktur JSON dari API
{
"id": 101,
"kode_transaksi": "TRX-20260115-001",
"tanggal": "2026-01-15T10:30:00",
"jumlah": 2,
"total_harga": 500000,
"status": "completed",
"user": {
"id": 5,
"username": "john_doe",
"alamat": "Jl. Merdeka No. 10"
},
"barang": {
"id": 12,
"nama": "Laptop Gaming",
"harga": 250000
}
}
Mapping Kolom DataGridView
| Header | DataPropertyName | Tipe |
|---|---|---|
| Kode Transaksi | kode_transaksi | string |
| Tanggal | tanggal | DateTime |
| Pembeli | user.username | string |
| Alamat | user.alamat | string |
| Nama Barang | barang.nama | string |
| Jumlah | jumlah | int |
| Total Harga | total_harga | decimal |
| Status | status | string |
user.username). Gunakan LINQ projection atau ViewModel untuk flatten data.Load Data dengan Custom Binding (ViewModel)
private async Task LoadTransaksiData()
{
try
{
dgvTransaksi.DataSource = null;
lblStatus.Text = "Loading data...";
var response = await _httpClient.GetAsync("transaksi");
var jsonResponse = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var list = JsonConvert.DeserializeObject<List<dynamic>>(jsonResponse);
var flatList = new List<TransaksiViewModel>();
foreach (var item in list)
{
flatList.Add(new TransaksiViewModel
{
KodeTransaksi = item.kode_transaksi,
Tanggal = DateTime.Parse(item.tanggal.ToString()),
Pembeli = item.user.username,
Alamat = item.user.alamat,
NamaBarang = item.barang.nama,
Jumlah = item.jumlah,
TotalHarga = item.total_harga,
Status = item.status
});
}
dgvTransaksi.DataSource = flatList;
// Format kolom
dgvTransaksi.Columns["TotalHarga"].DefaultCellStyle.Format = "Rp #,0";
dgvTransaksi.Columns["Tanggal"].DefaultCellStyle.Format = "dd MMM yyyy HH:mm";
// Warna status
dgvTransaksi.CellFormatting += (s, e) => {
if (dgvTransaksi.Columns[e.ColumnIndex].Name == "Status" && e.Value != null)
{
string st = e.Value.ToString().ToLower();
if (st == "completed")
e.CellStyle.BackColor = Color.LightGreen;
else if (st == "pending")
e.CellStyle.BackColor = Color.LightYellow;
else if (st == "cancelled")
e.CellStyle.BackColor = Color.LightPink;
}
};
lblStatus.Text = $"Menampilkan {flatList.Count} transaksi";
}
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}");
}
}
// ViewModel untuk flatten data nested
public class TransaksiViewModel
{
public string KodeTransaksi { get; set; }
public DateTime Tanggal { get; set; }
public string Pembeli { get; set; }
public string Alamat { get; set; }
public string NamaBarang { get; set; }
public int Jumlah { get; set; }
public decimal TotalHarga { get; set; }
public string Status { get; set; }
}
private async Task CreateTransaksi()
{
var data = new {
barang_id = selectedBarangId,
user_id = SessionManager.CurrentUserId,
jumlah = nudJumlah.Value,
alamat_pengiriman = txtAlamat.Text
};
var json = JsonConvert.SerializeObject(data);
var content = new StringContent(
json, Encoding.UTF8, "application/json");
var res = await _httpClient.PostAsync("transaksi", content);
if (res.IsSuccessStatusCode)
await LoadTransaksiData();
}
private async Task UpdateStatus(int id, string status)
{
var data = new { status = status };
var json = JsonConvert.SerializeObject(data);
var content = new StringContent(
json, Encoding.UTF8, "application/json");
var res = await _httpClient.PutAsync(
$"transaksi/{id}", content);
if (res.IsSuccessStatusCode)
await LoadTransaksiData();
}
private async Task DeleteTransaksi(int id)
{
var confirm = MessageBox.Show(
"Yakin hapus transaksi ini?",
"Konfirmasi",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (confirm == DialogResult.Yes)
{
var res = await _httpClient.DeleteAsync(
$"transaksi/{id}");
if (res.IsSuccessStatusCode)
await LoadTransaksiData();
}
}
private async Task GetDetailTransaksi(int id)
{
var res = await _httpClient.GetAsync(
$"transaksi/{id}");
var json = await res.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<dynamic>(json);
txtKodeTransaksi.Text = data.kode_transaksi;
txtTotal.Text = data.total_harga.ToString();
}
Model Produk
namespace MangsIpulApp.Models
{
public class Produk
{
public int id { get; set; }
public string nama { get; set; }
public string kode_produk { get; set; }
public decimal harga { get; set; }
public int stok { get; set; }
public string kategori { get; set; }
public string deskripsi { get; set; }
public DateTime created_at { get; set; }
}
}
Form Produk โ Load Data
private async Task LoadProdukData()
{
try
{
var response = await _httpClient.GetAsync("produk");
var json = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var list = JsonConvert.DeserializeObject<List<Produk>>(json);
dgvProduk.DataSource = list;
dgvProduk.Columns["harga"].DefaultCellStyle.Format = "Rp #,0";
// Warning stok rendah
dgvProduk.CellFormatting += (s, e) => {
if (dgvProduk.Columns[e.ColumnIndex].Name == "stok" && e.Value != null)
{
int stok = Convert.ToInt32(e.Value);
if (stok < 10)
e.CellStyle.ForeColor = Color.Red;
}
};
}
}
catch (Exception ex)
{
MessageBox.Show($"Error load produk: {ex.Message}");
}
}
Nama produk tidak boleh kosong ยท Harga harus > 0 ยท Stok minimal 0 ยท Kode produk harus unik ยท Konfirmasi sebelum delete
static class Program
{
[STAThread]
static void Main()
{
Application.ThreadException +=
new ThreadExceptionEventHandler(Application_ThreadException);
AppDomain.CurrentDomain.UnhandledException +=
new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new LoginForm());
}
private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
{
LogException(e.Exception);
MessageBox.Show($"Terjadi kesalahan: {e.Exception.Message}\n\nSilakan hubungi support.",
"Kesalahan Aplikasi", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var ex = (Exception)e.ExceptionObject;
LogException(ex);
MessageBox.Show($"Kesalahan fatal: {ex.Message}\nAplikasi akan ditutup.",
"Kesalahan Kritis", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private static void LogException(Exception ex)
{
string logPath = Path.Combine(Application.StartupPath, "logs",
$"error_{DateTime.Now:yyyyMMdd}.log");
Directory.CreateDirectory(Path.GetDirectoryName(logPath));
string log = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] " +
$"Message: {ex.Message}\n" +
$"Stack: {ex.StackTrace}\n" +
$"Source: {ex.Source}\n" +
new string('-', 50) + "\n";
File.AppendAllText(logPath, log);
}
}
Cara Membuat Installer dengan ClickOnce
.exe dan .application