可以實作的檔案類型包括「TXT、CSV、JSON、XML、XLS」,每一種檔案格式都有對應的IO函式庫。
在C#,可以使用Image函式庫來處理圖片,或者透過Stream 讀取Byte[]後轉成陣列
程式就像國文學科一樣,每個人都能寫出一篇作文,但是若要寫出一篇文筆簡潔的文章,需要對國文的修辭、文法、標點符號、成語、詞彙有一定的掌握。
同樣的,要對程式碼重構,對於「資料結構」和「物件導向」需要有非常高的掌握度,對於初學者來說並不容易
這次重構要處理的問題:
「自我介紹」程式經過七天的最終程式碼放在Github上,有需要可以自行下載取用
https://github.com/ted59438/2019ITHome
先看重構後的程式碼:
https://github.com/ted59438/2019ITHome/blob/master/IT_Day01/View/IntroductionForm.cs
在看重構前的程式碼:
https://github.com/ted59438/2019ITHome/blob/master/IT_Day01/View/IntroductionForm_before7Days.cs
我們來看看之前的程式碼,首先看到「按下自我介紹 (showIntroductionBtn_Click)」的事件程式碼,再看看 「按下保存個人資訊 (saveBtn_Click)」的事件程式碼。
// IntroductionForm.cs
TextBox[] allTextBox = new TextBox[] { nameTextBox, homeTownTextBox, birthdate_YearBox, birthdate_MonthBox, birthdate_DayBox };
string[] allTextBoxName = new string[]{LanguageResources.Name, LanguageResources.HomeTown,
LanguageResources.Birthday_Year, LanguageResources.Birthday_Month, LanguageResources.Birthday_Day };
StringBuilder errorMsg = new StringBuilder();
for (int i = 0; i < allTextBox.GetLength(0); i++)
{
if (string.IsNullOrEmpty(allTextBox[i].Text))
{
errorMsg.AppendLine(string.Format(LanguageResources.Message_PleaseInput, allTextBoxName[i]));
}
}
if (errorMsg.ToString() != "")
{
MessageBox.Show(errorMsg.ToString(), "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// 校驗日期格式是否正確
if (!Regex.IsMatch(birthdate_YearBox.Text, @"\d") || !Regex.IsMatch(birthdate_MonthBox.Text, @"\d") || !Regex.IsMatch(birthdate_DayBox.Text, @"\d"))
{
MessageBox.Show(LanguageResources.Message_BirthdayNeedNum, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
這段程式碼被拆成兩個函式方法
checkAllColumnIsNotEmpty():校驗所有欄位是不是都有輸入
checkDateIsValidate(): 校驗日期格式是否正確
// IntroductionForm.cs
private void checkAllColumnIsNotEmpty()
{
// 校驗每個欄位是否輸入
TextBox[] allTextBox = new TextBox[] { nameTextBox, homeTownTextBox, birthdate_YearBox, birthdate_MonthBox, birthdate_DayBox };
string[] allTextBoxName = new string[]{LanguageResources.Name, LanguageResources.HomeTown,
LanguageResources.Birthday_Year, LanguageResources.Birthday_Month, LanguageResources.Birthday_Day };
StringBuilder errorMsg = new StringBuilder();
for (int i = 0; i < allTextBox.GetLength(0); i++)
{
if (string.IsNullOrEmpty(allTextBox[i].Text))
{
errorMsg.AppendLine(string.Format(LanguageResources.Message_PleaseInput, allTextBoxName[i]));
}
}
if (photoBox.Image == null)
{
errorMsg.AppendLine(LanguageResources.Message_NoImage);
}
if (errorMsg.ToString() != "")
{
throw new Exception(errorMsg.ToString());
}
}
/// <summary>
/// 校驗日期格式是否正確
/// </summary>
private void checkDateIsValidate()
{
if (!Regex.IsMatch(birthdate_YearBox.Text, @"\d") || !Regex.IsMatch(birthdate_MonthBox.Text, @"\d") || !Regex.IsMatch(birthdate_DayBox.Text, @"\d"))
{
throw new Exception(LanguageResources.Message_BirthdayNeedNum);
}
}
畫面取得「姓名」、「家鄉」、「出生年月日」的程式碼也重複了
// IntroductionForm.cs
private void showIntroductionBtn_Click(object sender, EventArgs e)
{
// ... 前後程式碼省略
string name = nameTextBox.Text;
string homeTown = homeTownTextBox.Text;
int today_Year = DateTime.Today.Year;
int today_Month = DateTime.Today.Month;
int today_Day = DateTime.Today.Day;
// ... 前後程式碼省略
}
// IntroductionForm.cs
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
// ... 前後程式碼省略
string name = nameTextBox.Text;
string homeTown = homeTownTextBox.Text;
int birthDate_Year = int.Parse(birthdate_YearBox.Text);
int birthDate_Month = int.Parse(birthdate_MonthBox.Text);
int birthDate_Day = int.Parse(birthdate_DayBox.Text);
// ... 前後程式碼省略
}
建立一個新的C#類別檔案,取名為「IntroductionOBJ.cs」,把自我介紹的「姓名」、「家鄉」、「出生日期」、「相片」,用一個物件類別「IntroductionOBJ」封裝
// IntroductionOBJ.cs
using System;
namespace IT_Day01
{
public class IntroductionOBJ
{
/// <summary>
/// 姓名
/// </summary>
public string name { get; set; }
/// <summary>
/// 家鄉
/// </summary>
public string homeTown { get; set; }
/// <summary>
/// 生日
/// </summary>
public DateTime birthDate { get; set; }
/// <summary>
/// 相片
/// </summary>
public byte[] photo { get; set; }
}
}
在 自我介紹主視窗 (IntroductionForm.cs) 撰寫一個方法 (getIntroductionFromView()),每一次從畫面所有的欄位資料取得之後,以IntroductionOBJ 物件 存放
// IntroductionForm.cs
/// <summary>
/// 從畫面上的所有欄位取得自我介紹資訊
/// </summary>
/// <returns></returns>
private IntroductionOBJ getIntroductionFromView()
{
try
{
checkAllColumnIsNotEmpty();
checkDateIsValidate();
IntroductionOBJ introductionOBJ = new IntroductionOBJ();
introductionOBJ.name = nameTextBox.Text;
introductionOBJ.homeTown = homeTownTextBox.Text;
introductionOBJ.birthDate = new DateTime(int.Parse(birthdate_YearBox.Text), int.Parse(birthdate_MonthBox.Text), int.Parse(birthdate_DayBox.Text));
introductionOBJ.photo = PhotoHelper.ImageToBytes(photoBox.Image);
return introductionOBJ;
}
catch (Exception error)
{
throw new Exception(error.Message);
}
}
到目前重構完getIntroductionFromView()後的「按下自我介紹」跟「保存個人資訊」Click 事件的程式碼:
// 按下自我介紹
private void showIntroductionBtn_Click(object sender, EventArgs e)
{
IntroductionOBJ introductionOBJ = getIntroductionFromView();
// 計算年齡
int yearOld;
DateTime todayDate = DateTime.Today;
yearOld = today_Year - introductionOBJ.birthDate.Year;
if (todayDate.Month < introductionOBJ.birthDate.Month || (todayDate.Month == introductionOBJ.birthDate.Month && todayDate.Day < introductionOBJ.birthDate.Day))
{
yearOld = yearOld - 1;
}
string introductionText = string.Format(LanguageResources.Message_IntrouductionText, introductionOBJ.name, introductionOBJ.homeTown, yearOld);
MessageBox.Show(introductionText);
}
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
try
{
IntroductionOBJ introductionOBJ = getIntroductionFromView();
// ... 省略Json寫檔的程式碼
JObject introductionJson = new JObject();
introductionJson.Add("Name", introductionOBJ.name);
introductionJson.Add("HomeTown", introductionOBJ.homeTown);
introductionJson.Add("BirthDate",
string.Format("{0}-{1}-{2}", introductionOBJ.birthDate.Year,
introductionOBJ.birthDate.Month,
introductionOBJ.birthDate.Day));
string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
string jsonPath = Path.Combine(dirPath, "Introduction.json");
string imagePath = Path.Combine(dirPath, "Photo.jpeg");
// 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
// 檔案不存在,產生個人資訊的檔案
if (!File.Exists(jsonPath))
{
File.Create(jsonPath).Close();
}
// 保存個人資訊到JSON
File.WriteAllText(jsonPath, JsonConvert.SerializeObject(introductionJson));
// 保存個人大頭貼到Jpeg 圖片
photoBox.Image.Save(imagePath, ImageFormat.Jpeg);
}
catch (Exception error)
{
MessageBox.Show(error.Message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
新增專門負責讀寫檔的類別「FileHelper.cs」讓 WinForm 的「UI 邏輯」和 「JSON的讀寫操作」彼此不會互相牽扯。
// FileHelper.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text.RegularExpressions;
namespace IT_Day01
{
public class FileHelper
{
private static string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
private static string jsonPath = Path.Combine(dirPath, "Introduction.json");
private static string imagePath = Path.Combine(dirPath, "Photo.jpeg");
/// <summary>
/// 自我介紹 JSON讀檔流程
/// </summary>
/// <returns></returns>
public static IntroductionOBJ processRead()
{
IntroductionOBJ introductionOBJ;
JObject introductionJson;
if (!prepareRead())
{
return new IntroductionOBJ();
}
introductionJson = readFromJson();
if (!checkJsonIsVailed(introductionJson))
{
return new IntroductionOBJ();
}
introductionOBJ = getIntroductionJsonStr(introductionJson);
introductionOBJ.photo = PhotoHelper.readImageStreamFromFile(imagePath).ToArray() ;
return introductionOBJ;
}
/// <summary>
/// 讀檔前的檢查
/// </summary>
/// <returns></returns>
private static bool prepareRead()
{
if (!Directory.Exists(dirPath) || !File.Exists(jsonPath) || !File.Exists(imagePath))
{
return false;
}
return true;
}
/// <summary>
/// 讀取JSON文字
/// </summary>
/// <returns></returns>
private static JObject readFromJson()
{
string introductionJsonStr = File.ReadAllText(jsonPath);
JObject introductionJson = (JObject)JsonConvert.DeserializeObject(introductionJsonStr);
return introductionJson;
}
/// <summary>
/// 檢查內容結構是否被破壞
/// </summary>
/// <param name="introductionJson"></param>
/// <returns></returns>
private static bool checkJsonIsVailed(JObject introductionJson)
{
if (introductionJson == null ||
!introductionJson.ContainsKey("Name") || !introductionJson.ContainsKey("HomeTown") || !introductionJson.ContainsKey("BirthDate"))
{
return false;
}
else if (!Regex.IsMatch(introductionJson["BirthDate"].ToString().Split('-')[0], @"\d") ||
!Regex.IsMatch(introductionJson["BirthDate"].ToString().Split('-')[1], @"\d") ||
!Regex.IsMatch(introductionJson["BirthDate"].ToString().Split('-')[2], @"\d"))
{
return false;
}
else if (!File.Exists(imagePath))
{
return false;
}
return true;
}
/// <summary>
/// 取得自我介紹資訊
/// </summary>
/// <param name="introductionJson"></param>
/// <returns></returns>
private static IntroductionOBJ getIntroductionJsonStr(JObject introductionJson)
{
IntroductionOBJ introductionOBJ = new IntroductionOBJ();
introductionOBJ.name = introductionJson["Name"].ToString();
introductionOBJ.homeTown = introductionJson["HomeTown"].ToString();
introductionOBJ.birthDate = new DateTime(int.Parse(introductionJson["BirthDate"].ToString().Split('-')[0]),
int.Parse(introductionJson["BirthDate"].ToString().Split('-')[1]),
int.Parse(introductionJson["BirthDate"].ToString().Split('-')[2]));
return introductionOBJ;
}
/// <summary>
/// 自我介紹 寫檔流程
/// </summary>
/// <param name="introductionOBJ"></param>
public static void processSave(IntroductionOBJ introductionOBJ)
{
prepareWrite();
writeToJson(introductionOBJ);
saveImageToFile(introductionOBJ.photo);
}
private static void prepareWrite()
{
// 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
// 檔案不存在,產生個人資訊的檔案
if (!File.Exists(jsonPath))
{
File.Create(jsonPath).Close();
}
}
private static void writeToJson(IntroductionOBJ introductionOBJ)
{
JObject introductionJson = new JObject();
introductionJson.Add("Name", introductionOBJ.name);
introductionJson.Add("HomeTown", introductionOBJ.homeTown);
introductionJson.Add("BirthDate", string.Format("{0}-{1}-{2}", introductionOBJ.birthDate.Year,
introductionOBJ.birthDate.Month,
introductionOBJ.birthDate.Day));
File.WriteAllText(jsonPath, JsonConvert.SerializeObject(introductionJson));
}
private static void saveImageToFile(byte[] imageByte)
{
Image newImage = PhotoHelper.bytesToImage(imageByte);
newImage.Save(imagePath, ImageFormat.Jpeg);
}
}
}
如果要保存自我介紹的資訊,可透過先前封裝的「IntroductionOBJ」,從UI 傳到 處理讀寫檔的
以下分別是「自我介紹主畫面載入後(IntroductionForm_Load)」重構前後的程式碼
/// <summary>
/// 顯示邀請自我介紹的文字
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void IntroductionForm_Load(object sender, EventArgs e)
{
loadSaveIntroduction();
MessageBox.Show(LanguageResources.FormStart);
}
/// <summary>
/// 將自我介紹的資訊顯示到畫面上
/// </summary>
private void loadSaveIntroduction()
{
string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
string filePath = Path.Combine(dirPath, "Introduction.json");
string imagePath = Path.Combine(dirPath, "Photo.jpeg");
// 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
if (!Directory.Exists(dirPath) || !File.Exists(filePath) || !File.Exists(imagePath))
{
return;
}
// 從JSON檔讀取先前保存的個人資訊
string introductionJsonStr = File.ReadAllText(filePath);
JObject introductionJson = (JObject)JsonConvert.DeserializeObject(introductionJsonStr);
//讀完資料發現資料空白、結構有缺漏、結構被破壞,不要讀取
if (introductionJson == null ||
!introductionJson.ContainsKey("Name") || !introductionJson.ContainsKey("HomeTown") || !introductionJson.ContainsKey("BirthDate"))
{
return;
}
else
{
nameTextBox.Text = introductionJson["Name"].ToString();
homeTownTextBox.Text = introductionJson["HomeTown"].ToString();
birthdate_YearBox.Text = introductionJson["BirthDate"].ToString().Split('-')[0];
birthdate_MonthBox.Text = introductionJson["BirthDate"].ToString().Split('-')[1];
birthdate_DayBox.Text = introductionJson["BirthDate"].ToString().Split('-')[2];
}
photoBox.Image = Image.FromFile(imagePath);
}
/// <summary>
/// 顯示邀請自我介紹的文字
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void IntroductionForm_Load(object sender, EventArgs e)
{
loadSaveIntroduction();
MessageBox.Show(LanguageResources.FormStart);
}
/// <summary>
/// 將自我介紹的資訊顯示到畫面上
/// </summary>
private void loadSaveIntroduction()
{
IntroductionOBJ introductionOBJ = FileHelper.processRead();
nameTextBox.Text = introductionOBJ.name;
homeTownTextBox.Text = introductionOBJ.homeTown;
birthdate_YearBox.Text = introductionOBJ.birthDate.Year.ToString();
birthdate_MonthBox.Text = introductionOBJ.birthDate.Month.ToString();
birthdate_DayBox.Text = introductionOBJ.birthDate.Day.ToString();
if (introductionOBJ.photo != null)
photoBox.Image = PhotoHelper.bytesToImage(introductionOBJ.photo);
}
以下是「保存個人資訊」 重構前後的程式碼
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
try
{
IntroductionOBJ introductionOBJ = getIntroductionFromView();
JObject introductionJson = new JObject();
introductionJson.Add("Name", introductionOBJ.name);
introductionJson.Add("HomeTown", introductionOBJ.homeTown);
introductionJson.Add("BirthDate",
string.Format("{0}-{1}-{2}", introductionOBJ.birthDate.Year,
introductionOBJ.birthDate.Month,
introductionOBJ.birthDate.Day));
string dirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
string jsonPath = Path.Combine(dirPath, "Introduction.json");
string imagePath = Path.Combine(dirPath, "Photo.jpeg");
// 每次讀寫檔之前,檢查路徑的資料夾與檔案是否存在,避免發生路徑不存在的錯誤
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
// 檔案不存在,產生個人資訊的檔案
if (!File.Exists(jsonPath))
{
File.Create(jsonPath).Close();
}
// 保存個人資訊到JSON
File.WriteAllText(jsonPath, JsonConvert.SerializeObject(introductionJson));
// 保存個人大頭貼到Jpeg 圖片
photoBox.Image.Save(imagePath, ImageFormat.Jpeg);
}
catch (Exception error)
{
MessageBox.Show(error.Message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 按下「保存個人資訊」
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveBtn_Click(object sender, EventArgs e)
{
try
{
IntroductionOBJ introductionOBJ = getIntroductionFromView();
FileHelper.processSave(introductionOBJ);
MessageBox.Show("保存完成!", "訊息", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception error)
{
MessageBox.Show(error.Message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
如果將PictureBox的Image存到IntroductionOBJ,存檔的時候會發生「在 GDI+ 中發生泛型錯誤」,原因是PictureBox透過Image.FromFile佔用了圖片檔,所以我們改以Byte[]型態儲存圖片。
Byte、Image跟Stream的轉換處理,我們另外抽離到新的類別檔案 PhotoHelper.cs introductionOBJ.photo = Image.FromFile(imagePath);
// PhotoHelper.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
namespace IT_Day01
{
public class PhotoHelper
{
/// <summary>
/// 圖片轉Bytes
/// </summary>
/// <param name="img"></param>
/// <returns></returns>
public static byte[] ImageToBytes(Image img)
{
MemoryStream memoryStream = new MemoryStream();
img.Save(memoryStream, ImageFormat.Jpeg);
return memoryStream.ToArray();
}
/// <summary>
/// Bytes轉圖片
/// </summary>
/// <param name="imgBytes"></param>
/// <returns></returns>
public static Image bytesToImage(byte[] imgBytes)
{
MemoryStream memoryStream = new MemoryStream(imgBytes);
return Image.FromStream(memoryStream);
}
public static MemoryStream readImageStreamFromFile(string imagePath)
{
MemoryStream memoryStream = new MemoryStream();
Image imageFile = Image.FromFile(imagePath);
imageFile.Save(memoryStream, ImageFormat.Jpeg);
// 釋放資源,避免圖片檔案被占用
imageFile.Dispose();
return memoryStream;
}
}
}
設定圖片到PictureBox上面
photoBox.Image = PhotoHelper.bytesToImage(introductionOBJ.photo);
將PictureBox的圖片以Byte[]型態存到IntroductionOBJ
introductionOBJ.photo = PhotoHelper.ImageToBytes(photoBox.Image);
這次的重構過程沒有用到太深的物件導向觀念(繼承、抽象、多型),主要用了「封裝」和「靜態方法」抽離UI和非UI的程式邏輯。
雖然對於一個小程式的程式碼量而言,重構的效益相對沒有像大系統一樣明顯,但是如果是一個真正的資訊系統,由於包含非常多模組,每個模組至少有上千行的程式碼,如果UI(View)、資料處理邏輯(Controller)、資料實體(Entity)全部混在一起,一旦程式碼出問題,需要付出非常大的維護成本。
在幫「自我介紹程式」畫下句點之前,我會分享一些自身程式教、學相關的非技術議題,以及這支專案的相關衍伸議題和應用。