Bangun Aplikasi MangsIpul
Panduan komprehensif pengembangan aplikasi Desktop Windows Forms dengan arsitektur REST API. Materi mencakup dari persiapan lingkungan, desain UI/UX, hingga implementasi CRUD dan deployment.
Persiapan & Instalasi Tools
Membangun fondasi lingkungan pengembangan yang solid
🎯 Mengapa Visual Studio 2022 Community?
Visual Studio 2022 Community adalah IDE gratis dan full-featured yang menjadi standar industri untuk pengembangan .NET. Berikut 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 tanpa perlu coding manual posisi kontrol
- IntelliCode - AI-assisted coding yang memprediksi kode berdasarkan konteks, meningkatkan produktivitas hingga 30%
- NuGet Package Manager - Manajemen library eksternal terintegrasi, memudahkan instalasi Newtonsoft.Json, RestSharp, dll
- Live Share - Kolaborasi real-time dengan tim untuk pair programming
📥 Prosedur Download & Instalasi Lengkap
Kunjungi situs resmi Microsoft
Buka visualstudio.microsoft.com/downloads menggunakan browser (Chrome/Edge/Firefox).
Pilih edisi Community
Scroll ke bawah, temukan bagian "Visual Studio Community 2022", lalu klik tombol Free Download. Edisi Community gratis untuk individu, open source, dan tim kecil (maks 5 developer).
Jalankan Installer
File vs_community.exe akan terdownload. Klik kanan → Run as Administrator (penting untuk instalasi komponen sistem).
Pilih Workload yang Tepat
Centang ".NET desktop development". Workload ini akan menginstal:
- .NET Framework 4.8 SDK & .NET 6/7/8 SDK
- Windows Forms Designer
- MSBuild compiler
- Debugging tools
Klik Install & Tunggu Proses
Proses instalasi memakan waktu 10-30 menit tergantung kecepatan internet. Pastikan koneksi stabil karena total download sekitar 5-8 GB.
✅ Verifikasi Instalasi
✔ Visual Studio 2022 muncul di Start Menu
✔ Bisa membuat project baru dengan template "Windows Forms App"
✔ NuGet Package Manager bisa diakses via Tools → NuGet Package Manager
Arsitektur Proyek
Membangun struktur kode yang scalable dan maintainable
Arsitektur aplikasi desktop modern memisahkan UI (Presentation Layer), Logika Bisnis (Business Logic), dan Akses Data (Data Access). Berikut struktur folder yang direkomendasikan untuk proyek MangsIpul:
MangsIpulApp/
├── 📁 Forms/ # Semua Form UI
│ ├── LoginForm.cs
│ ├── MainDashboard.cs
│ ├── TransaksiForm.cs
│ └── ProdukForm.cs
│
├── 📁 Models/ # Class untuk merepresentasikan data
│ ├── User.cs
│ ├── Barang.cs
│ ├── Transaksi.cs
│ └── ApiResponse.cs
│
├── 📁 Services/ # Service layer (API, Session, dll)
│ ├── ApiService.cs # HttpClient terpusat
│ ├── SessionManager.cs # Manajemen token & user session
│ └── AuthService.cs # Login/logout logic
│
├── 📁 Helpers/ # Utility classes
│ ├── FormHelper.cs # Navigasi form
│ └── ValidationHelper.cs # Validasi input
│
├── 📁 Resources/ # Assets (gambar, icon)
│ └── logo.png
│
├── app.config # Konfigurasi aplikasi (base URL, dll)
└── Program.cs # Entry point aplikasi
Berisi semua file .cs untuk UI. Setiap form hanya bertanggung jawab untuk menampilkan data dan mengirim input user. Tidak boleh ada logika bisnis atau akses database langsung di sini.
Plain C# class yang merepresentasikan struktur data (mirip dengan JSON dari API). Contoh: User punya property Id, Username, Email.
Tempat semua logika komunikasi dengan API, manajemen session, dan business rules. Form hanya memanggil method dari service ini.
Function-function utility yang reusable, seperti format tanggal, validasi email, atau navigasi antar form.
💡 Prinsip Penting: Pisahkan UI dari logika! Jika nanti ingin migrasi dari WinForms ke WPF atau MAUI, Anda hanya perlu mengganti folder Forms, sementara Models dan Services bisa digunakan ulang 100%.
Inisialisasi Proyek
Langkah membuat proyek baru di Visual Studio
Langkah-langkah Membuat Proyek Baru:
- Buka Visual Studio 2022 → Klik "Create a new project"
- Pilih template "Windows Forms App (.NET Framework)" atau "Windows Forms App" (.NET 6/7/8)
- Isi:
- Project name: MangsIpulApp
- Location: Pilih folder (misal: D:\Projects\)
- Framework: .NET 6.0 (LTS) atau .NET 8.0
- Klik "Create"
Install NuGet Packages yang Diperlukan:
# Buka Tools → NuGet Package Manager → Package Manager Console
# Kemudian jalankan perintah berikut:
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
Newtonsoft.Json digunakan untuk serialize/deserialize JSON dari API.
RestSharp alternatif HttpClient yang lebih sederhana.
Microsoft.Extensions.Configuration untuk membaca file konfigurasi seperti appsettings.json.
Kode Program.cs (Entry Point):
using System;
using System.Windows.Forms;
namespace MangsIpulApp
{
internal static class Program
{
/// <summary>
/// Entry point utama aplikasi.
/// </summary>
[STAThread]
static void Main()
{
// Mengaktifkan visual styles untuk tampilan modern
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// Menampilkan form login terlebih dahulu
// LoginForm akan mengecek apakah user sudah login sebelumnya
Application.Run(new LoginForm());
}
}
}
[STAThread] adalah attribute yang wajib ada untuk aplikasi Windows Forms.
STA = Single-Threaded Apartment, diperlukan untuk komponen COM seperti Clipboard dan Drag-Drop.
Desain Form Login (Step-by-Step)
Membuat antarmuka login yang profesional dan user-friendly
Konfigurasi Jendela Form
Buka file LoginForm.cs di designer, lalu set properti berikut di Properties Window:
- Text "MangsIpul Login System"
- Size 450, 600
- StartPosition CenterScreen
- FormBorderStyle FixedDialog
- MaximizeBox False
- BackColor White atau #F3F4F6
Menambahkan Komponen Input
Label "Username" - Drag dari Toolbox → Label, set Text = "Email / Username"
TextBox Username - Drag TextBox, set (Name) = txtUsername, set placeholder menggunakan event Enter/Leave
Label "Password" - Sama seperti username
TextBox Password - Drag TextBox, set:
- (Name) =
txtPassword - UseSystemPasswordChar = True ← WAJIB! (menampilkan bullet ●●● bukan teks asli)
CheckBox "Show Password" - Untuk toggle visibility password
Button Login - Drag Button, set properti:
- (Name) =
btnLogin - Text = "MASUK" atau "LOGIN"
- BackColor = #6d28d9 (Purple)
- ForeColor = White
- FlatStyle = Flat, lalu set FlatAppearance.BorderSize = 0
- Cursor = Hand (ubah pointer saat hover)
Menggunakan TableLayoutPanel untuk Responsif
Agar form terlihat rapi dan responsif saat di-resize (walau FixedDialog, tetap bagus untuk maintain layout), gunakan TableLayoutPanel:
- Drag TableLayoutPanel ke form, set Dock = Fill
- Atur jumlah baris dan kolom (misal 5 baris, 2 kolom)
- Masukkan Label dan TextBox ke dalam sel yang sesuai
- Atur properti ColumnStyles dan RowStyles untuk persentase lebar
💡 Fitur Tambahan: Toggle Show Password
private void chkShowPassword_CheckedChanged(object sender, EventArgs e)
{
// Jika checkbox dicentang, tampilkan password asli
// Jika tidak, tampilkan bullet
txtPassword.UseSystemPasswordChar = !chkShowPassword.Checked;
}
Implementasi API Login + Session Manager
Menghubungkan form login dengan backend REST API
1. Membuat SessionManager (Service)
Buat file baru Services/SessionManager.cs:
using System;
namespace MangsIpulApp.Services
{
public static class SessionManager
{
// Menyimpan token JWT yang didapat dari API
public static string BearerToken { get; set; }
// Informasi user yang sedang login
public static string CurrentUserId { get; set; }
public static string CurrentUsername { get; set; }
public static string CurrentUserEmail { get; set; }
public static string CurrentUserRole { get; set; }
// Cek apakah user sudah login
public static bool IsLoggedIn => !string.IsNullOrEmpty(BearerToken);
// Logout: hapus semua session
public static void Logout()
{
BearerToken = null;
CurrentUserId = null;
CurrentUsername = null;
CurrentUserEmail = null;
CurrentUserRole = null;
}
}
}
2. Membuat ApiService (HttpClient Handler)
Best Practice: Gunakan satu instance HttpClient untuk seluruh aplikasi untuk menghindari socket exhaustion.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace MangsIpulApp.Services
{
public class ApiService
{
private static HttpClient _client;
private static readonly object _lock = new object();
// Singleton pattern untuk HttpClient
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;
}
}
}
// Method untuk menambahkan token Authorization ke setiap request
public static void SetAuthorizationHeader()
{
if (SessionManager.IsLoggedIn)
{
Client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", SessionManager.BearerToken);
}
}
// Hapus authorization header saat logout
public static void RemoveAuthorizationHeader()
{
Client.DefaultRequestHeaders.Authorization = null;
}
}
}
3. Model untuk Response Login
Buat file 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; }
[JsonProperty("errors")]
public object Errors { get; set; }
}
}
4. Implementasi Method Login di LoginForm.cs
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
using MangsIpulApp.Models;
using MangsIpulApp.Services;
namespace MangsIpulApp.Forms
{
public partial class LoginForm : Form
{
private readonly HttpClient _httpClient;
public LoginForm()
{
InitializeComponent();
_httpClient = ApiService.Client;
}
private async void btnLogin_Click(object sender, EventArgs e)
{
// Validasi input tidak boleh kosong
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;
}
// Disable button sambil proses login
btnLogin.Enabled = false;
btnLogin.Text = "Memproses...";
// Buat request object
var loginRequest = new LoginRequest
{
email = txtUsername.Text.Trim(),
password = txtPassword.Text
};
try
{
// Serialize ke JSON
var json = JsonConvert.SerializeObject(loginRequest);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
// Kirim POST request ke endpoint login
var response = await _httpClient.PostAsync("login", content);
var responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
// Parse response JSON
var loginResult = JsonConvert.DeserializeObject<LoginResponse>(responseBody);
// Simpan token dan data user ke SessionManager
SessionManager.BearerToken = loginResult.Token;
SessionManager.CurrentUserId = loginResult.User.Id.ToString();
SessionManager.CurrentUsername = loginResult.User.Username;
SessionManager.CurrentUserEmail = loginResult.User.Email;
SessionManager.CurrentUserRole = loginResult.User.Role;
// Set header Authorization untuk request selanjutnya
ApiService.SetAuthorizationHeader();
// Tampilkan pesan sukses
MessageBox.Show($"Selamat datang, {loginResult.User.Username}!",
"Login Berhasil", MessageBoxButtons.OK, MessageBoxIcon.Information);
// Buka Main Dashboard
var dashboard = new MainDashboard();
dashboard.Show();
// Sembunyikan form login, jangan di Close() karena akan mengakhiri aplikasi
this.Hide();
}
else
{
// Handle error dari API (401, 404, 500, dll)
try
{
var errorResult = JsonConvert.DeserializeObject<ErrorResponse>(responseBody);
MessageBox.Show($"Login gagal: {errorResult?.Message ?? "Username atau password salah"}",
"Error Login", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch
{
MessageBox.Show($"Login gagal: Server mengembalikan error ({(int)response.StatusCode})",
"Error Login", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
catch (HttpRequestException ex)
{
MessageBox.Show($"Tidak dapat terhubung ke server.\nPeriksa koneksi internet Anda.\nDetail: {ex.Message}",
"Network Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch (TaskCanceledException)
{
MessageBox.Show("Request timeout. Server mungkin sedang sibuk atau koneksi lambat.",
"Timeout", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
catch (Exception ex)
{
MessageBox.Show($"Terjadi kesalahan: {ex.Message}",
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// Enable button kembali
btnLogin.Enabled = true;
btnLogin.Text = "LOGIN";
}
}
// Event handler untuk tombol Close
private void btnClose_Click(object sender, EventArgs e)
{
Application.Exit();
}
}
}
🔄 Alur Logika Login
1. User klik "Login"
↓ Validasi input (tidak boleh kosong)
2. Buat object LoginRequest {email, password}
↓ Serialize ke JSON
3. Kirim POST request ke /api/login
↓ Tunggu response dari server
4. Jika success (200):
→ Parse JSON response
→ Simpan token ke SessionManager
→ Set Authorization header untuk request selanjutnya
→ Buka MainDashboard
→ Sembunyikan LoginForm
5. Jika gagal:
→ Tampilkan pesan error dari server
Modul Transaksi - Desain & Data Binding
Menampilkan data transaksi dengan DataGridView dan mapping ke API
Struktur Data Transaksi 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, Jakarta"
},
"barang": {
"id": 12,
"nama": "Laptop Gaming",
"harga": 250000
}
}
Setting Kolom DataGridView (Manual Binding)
Karena struktur JSON memiliki nested object (user.username, barang.nama), kita perlu mapping manual.
| Display Header | DataPropertyName | Tipe Data | Penjelasan |
|---|---|---|---|
| Kode Transaksi | kode_transaksi | string | ID unik transaksi |
| Tanggal | tanggal | DateTime | Waktu transaksi |
| Pembeli | user.username | string | Nama pembeli (nested) |
| Alamat | user.alamat | string | Alamat pengiriman (nested) |
| Nama Barang | barang.nama | string | Nama produk (nested) |
| Jumlah | jumlah | int | Quantity barang |
| Total Harga | total_harga | decimal | Format: Rp 0,000 |
| Status | status | string | pending, completed, cancelled |
⚠️ Catatan: DataGridView tidak bisa langsung binding ke nested property (user.username). Kita harus menggunakan Custom Binding atau LINQ projection untuk flatten data.
Kode untuk Load Data Transaksi dengan Custom Binding
using Newtonsoft.Json;
using MangsIpulApp.Services;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MangsIpulApp.Forms
{
public partial class TransaksiForm : Form
{
private readonly HttpClient _httpClient;
public TransaksiForm()
{
InitializeComponent();
_httpClient = ApiService.Client;
Load += async (s, e) => await LoadTransaksiData();
}
private async Task LoadTransaksiData()
{
try
{
// Tampilkan loading indicator
dgvTransaksi.DataSource = null;
lblStatus.Text = "Loading data...";
// Panggil API GET /transaksi
var response = await _httpClient.GetAsync("transaksi");
var jsonResponse = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
// Deserialize ke list of dynamic atau model
var transaksiList = JsonConvert.DeserializeObject<List<dynamic>>(jsonResponse);
// Flatten data untuk ditampilkan di DataGridView
var flatList = new List<TransaksiViewModel>();
foreach (var item in transaksiList)
{
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
});
}
// Binding ke DataGridView
dgvTransaksi.DataSource = flatList;
// Format kolom
FormatDataGridView();
lblStatus.Text = $"Menampilkan {flatList.Count} transaksi";
}
else
{
lblStatus.Text = $"Error: {response.StatusCode}";
MessageBox.Show($"Gagal load data: {jsonResponse}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (Exception ex)
{
lblStatus.Text = "Error loading data";
MessageBox.Show($"Error: {ex.Message}", "Exception",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void FormatDataGridView()
{
// Set alignment untuk kolom angka
if (dgvTransaksi.Columns["TotalHarga"] != null)
dgvTransaksi.Columns["TotalHarga"].DefaultCellStyle.Format = "Rp #,0";
if (dgvTransaksi.Columns["Tanggal"] != null)
dgvTransaksi.Columns["Tanggal"].DefaultCellStyle.Format = "dd MMM yyyy HH:mm";
// Warna background berdasarkan status
dgvTransaksi.CellFormatting += (s, e) => {
if (dgvTransaksi.Columns[e.ColumnIndex].Name == "Status" && e.Value != null)
{
string status = e.Value.ToString().ToLower();
if (status == "completed")
e.CellStyle.BackColor = System.Drawing.Color.LightGreen;
else if (status == "pending")
e.CellStyle.BackColor = System.Drawing.Color.LightYellow;
else if (status == "cancelled")
e.CellStyle.BackColor = System.Drawing.Color.LightPink;
}
};
}
}
// ViewModel untuk flatten data
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; }
}
}
CRUD Transaksi dengan API
Create, Read, Update, Delete data transaksi melalui REST API
// Menambah transaksi baru
private async Task CreateTransaksi()
{
var newTransaksi = new {
barang_id = selectedBarangId,
user_id = SessionManager.CurrentUserId,
jumlah = nudJumlah.Value,
alamat_pengiriman = txtAlamat.Text
};
var json = JsonConvert.SerializeObject(newTransaksi);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("transaksi", content);
if(response.IsSuccessStatusCode)
await LoadTransaksiData();
}
// Update status transaksi
private async Task UpdateStatus(int id, string status)
{
var updateData = new { status = status };
var json = JsonConvert.SerializeObject(updateData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PutAsync($"transaksi/{id}", content);
if(response.IsSuccessStatusCode)
await LoadTransaksiData();
}
// Hapus transaksi
private async Task DeleteTransaksi(int id)
{
var confirm = MessageBox.Show("Yakin hapus transaksi ini?",
"Konfirmasi", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if(confirm == DialogResult.Yes)
{
var response = await _httpClient.DeleteAsync($"transaksi/{id}");
if(response.IsSuccessStatusCode)
await LoadTransaksiData();
}
}
// Ambil detail transaksi
private async Task GetDetailTransaksi(int id)
{
var response = await _httpClient.GetAsync($"transaksi/{id}");
var json = await response.Content.ReadAsStringAsync();
var transaksi = JsonConvert.DeserializeObject<dynamic>(json);
txtKodeTransaksi.Text = transaksi.kode_transaksi;
txtTotal.Text = transaksi.total_harga.ToString();
}
Modul Produk - Manajemen Barang
CRUD data produk dengan validasi stok dan harga
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
public partial class ProdukForm : Form
{
private async Task LoadProdukData()
{
try
{
var response = await _httpClient.GetAsync("produk");
var json = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var produkList = JsonConvert.DeserializeObject<List<Produk>>(json);
dgvProduk.DataSource = produkList;
// Format kolom harga
dgvProduk.Columns["harga"].DefaultCellStyle.Format = "Rp #,0";
// Color warning jika stok < 10
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 = System.Drawing.Color.Red;
}
};
}
}
catch (Exception ex)
{
MessageBox.Show($"Error load produk: {ex.Message}");
}
}
}
✅ Validasi yang perlu ditambahkan pada form produk:
- Nama produk tidak boleh kosong
- Harga harus > 0
- Stok minimal 0 (tidak negatif)
- Kode produk harus unik (cek ke API terlebih dahulu)
- Konfirmasi sebelum delete
Global Error Handling & Logging
Menangani exception secara profesional di level aplikasi
// Di Program.cs - Tambahkan global exception handlers
static class Program
{
[STAThread]
static void Main()
{
// Handle unhandled exceptions dari UI thread
Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
// Handle unhandled exceptions dari non-UI threads
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 logContent = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] " +
$"Message: {ex.Message}\n" +
$"Stack Trace: {ex.StackTrace}\n" +
$"Source: {ex.Source}\n" +
new string('-', 50) + "\n";
File.AppendAllText(logPath, logContent);
}
}
Deployment & Packaging
Mempublikasikan aplikasi ke installer dan distribusi
Cara Membuat Installer dengan ClickOnce
- Klik kanan project → Publish
- Pilih Folder → Browse lokasi publish
- Pilih Install from folder
- Set Installation Mode = "The application is available online only" atau "Offline"
- Klik Finish → Publish
- Hasil installer akan berada di folder publish, file .exe dan .application
🎯 Checklist Sebelum Deployment:
- ✓ Application manifest sudah di-sign
- ✓ Base URL API diubah ke production
- ✓ Connection string tidak hardcode
- ✓ Build dalam mode Release
- ✓ Semua dependency sudah terpackage
- ✓ Testing di environment bersih