361 Chương 9: File, thư mục, và I/O TreeView rất thích hợp với cách tiếp cận này vì nó cung cấp sự kiện BeforeExpand (sự kiện này phát sinh trước khi một cấp mới được hiển thị). Bạn có thể sử dụng một placeholder (như dấu hoa thị hay nút rỗng) trong tất cả các nhánh chưa được thêm vào. Điều này cho phép bạn thêm vào các phần của cây thư mục khi chúng được hiển thị. Để sử dụng kiểu giải pháp này, bạn cần ba yếu tố sau: • Phương thức Fill —thêm một cấp mới vào một thư mục. Bạn sẽ sử dụng phương thức này để thêm vào các cấp khi chúng được mở rộng. • Phương thức thụ lý sự kiện Form.Load —sử dụng Fill để tạo cây với cấp đầu tiên. • Phương thức thụ lý sự kiện TreeView.BeforeExpand —phản ứng khi người dùng mở rộng một nút và gọi Fill nếu thông tin của thư mục này chưa được thêm. Dưới đây là phần mã cho form: using System; using System.IO; using System.Drawing; using System.Windows.Forms; public class DirectoryTree : System.Windows.Forms.Form { private System.Windows.Forms.TreeView treeDirectory; // (Bỏ qua phần mã designer.) private void Fill(TreeNode dirNode) { DirectoryInfo dir = new DirectoryInfo(dirNode.FullPath); // Một ngoại lệ có thể bị ném nếu bạn không có // đủ quyền thao tác trên file hay thư mục. // Bạn có thể bắt và bỏ qua ngoại lệ này. foreach (DirectoryInfo dirItem in dir.GetDirectories()) { // Thêm nút giả cho thư mục. TreeNode newNode = new TreeNode(dirItem.Name); dirNode.Nodes.Add(newNode); newNode.Nodes.Add("*"); } } private void DirectoryTree_Load(object sender, System.EventArgs e) { 362 Chương 9: File, thư mục, và I/O // Thiết lập nút đầu tiên. TreeNode rootNode = new TreeNode("C:\\"); treeDirectory.Nodes.Add(rootNode); // Thêm cấp thứ nhất và mở rộng nó. Fill(rootNode); treeDirectory.Nodes[0].Expand(); } private void treeDirectory_BeforeExpand(object sender, System.Windows.Forms.TreeViewCancelEventArgs e) { // Nếu tìm thấy một nút giả, xóa nó và đọc các thư mục thật. if (e.Node.Nodes[0].Text == "*") { e.Node.Nodes.Clear(); Fill(e.Node); } } } Hình 9.1 Một cây thư mục với TreeView 363 Chương 9: File, thư mục, và I/O 7. 7. Đ c và ghi file văn b nọ ả Đ c và ghi file văn b nọ ả Bạn cần ghi dữ liệu vào một file văn bản theo kiểu mã hóa ASCII , Unicode , hay UTF-8 . Tạo một đối tượng System.IO.FileStream tham chiếu đến file. Để ghi file, hãy gói FileStream trong một System.IO.StreamWriter và sử dụng phương thức nạp chồng Write . Để đọc file, hãy gói FileStream trong một System.IO.StreamReader và sử dụng phương thức Read hay ReadLine . .NET cho phép bạn ghi hay đọc văn bản bằng lớp StreamWriter và StreamReader . Khi ghi dữ liệu với StreamWriter , hãy sử dụng phương thức StreamWriter.Write . Phương thức này được nạp chồng để hỗ trợ tất cả các kiểu dữ liệu thông thường trong C# .NET, bao gồm chuỗi, ký tự, số nguyên, số thực dấu chấm động, số thập phân, Tuy nhiên, phương thức Write luôn chuyển dữ liệu thành văn bản. Nếu muốn chuyển văn bản này trở về kiểu ban đầu thì bạn nên sử dụng WriteLine để bảo đảm mỗi giá trị được đặt trên một dòng riêng. Có nhiều cách mô tả một chuỗi dưới dạng nhị phân, tùy thuộc vào cách mã hóa. Các kiểu mã hóa thông thường là: • ASCII—sử dụng 7 bit để mã hóa mỗi ký tự trong chuỗi. Dữ liệu được mã hóa theo ASCII không thể chứa các ký tự Unicode mở rộng. Khi sử dụng kiểu mã hóa ASCII trong .NET, các bit được đệm thêm để mảng byte kết quả sẽ có 1 byte cho mỗi ký tự. • Full Unicode, hay UTF-16—sử dụng 16 bit để mã hóa mỗi ký tự trong chuỗi, nên mảng byte kết quả sẽ có 2 byte cho mỗi ký tự. • UTF-7 Unicode—sử dụng 7 bit cho các ký tự ASCII bình thường và nhiều cặp 7 bit cho các ký tự mở rộng. Kiểu mã hóa này chủ yếu dùng cho các giao thức 7 bit, chẳng hạn mail. • UTF-8 Unicode—sử dụng 8 bit cho các ký tự ASCII bình thường và nhiều cặp 8 bit cho các ký tự mở rộng. Mảng byte kết quả sẽ có 1 byte cho mỗi ký tự (giả sử không có ký tự mở rộng). .NET cung cấp một lớp cho mỗi kiểu mã hóa trong không gian tên System.Text . Khi sử dụng StreamReader và StreamWriter , bạn có thể chỉ định kiểu mã hóa hoặc sử dụng kiểu mặc định là UTF-8. Khi đọc thông tin, sử dụng phương thức Read hay ReadLine của lớp StreamReader . Phương thức Read đọc một ký tự, hay số ký tự do bạn chỉ định, và trả về một ký tự hay mảng ký tự. Phương thức ReadLine trả về một chuỗi chứa toàn bộ nội dung một hàng. Ứng dụng Console dưới đây minh họa việc ghi và đọc một file văn bản: using System; using System.IO; using System.Text; public class TextFileTest { 364 Chương 9: File, thư mục, và I/O private static void Main() { // Tạo file mới. FileStream fs = new FileStream("test.txt", FileMode.Create); // Tạo một writer và chỉ định kiểu mã hóa. // Kiểu mặc định (UTF-8) hỗ trợ ký tự Unicode, // nhưng mã hóa các ký tự chuẩn giống như ASCII StreamWriter w = new StreamWriter(fs, Encoding.UTF8); // Ghi một số thập phân, một chuỗi, và một ký tự. w.WriteLine(124.23M); w.WriteLine("Test string"); w.WriteLine('!'); // Bảo đảm tất cả dữ liệu được ghi từ buffer. w.Flush(); // Đóng file. w.Close(); fs.Close(); Console.WriteLine("Press Enter to read the information."); Console.ReadLine(); // Mở file trong chế độ chỉ-đọc. fs = new FileStream("test.txt", FileMode.Open); StreamReader r = new StreamReader(fs, Encoding.UTF8); // Đọc dữ liệu và chuyển nó về kiểu thích hợp. Console.WriteLine(Decimal.Parse(r.ReadLine())); Console.WriteLine(r.ReadLine()); Console.WriteLine(Char.Parse(r.ReadLine())); r.Close(); fs.Close(); 365 Chương 9: File, thư mục, và I/O Console.ReadLine(); } } 8. 8. Đ c và ghi file nh phânọ ị Đ c và ghi file nh phânọ ị Bạn cần ghi dữ liệu vào file nhị phân với kiểu dữ liệu mạnh. Tạo một đối tượng System.IO.FileStream tham chiếu đến file. Để ghi file, hãy gói FileStream trong một System.IO.BinaryWriter và sử dụng phương thức nạp chồng Write . Để đọc file, hãy gói FileStream trong một System.IO.BinaryReader và sử dụng phương thức Read phù hợp với kiểu dữ liệu. .NET cho phép bạn ghi hay đọc dữ liệu nhị phân bằng lớp BinaryWriter và BinaryReader . Khi ghi dữ liệu với BinaryWriter , hãy sử dụng phương thức BinaryWriter.Write . Phương thức này được nạp chồng để hỗ trợ tất cả kiểu dữ liệu thông thường trong C# .NET, bao gồm chuỗi, ký tự, số nguyên, số thực dấu chấm động, số thập phân, Thông tin sau đó được mã hóa thành một dãy các byte và ghi vào file. Bạn có thể chỉ định kiểu mã hóa cho chuỗi bằng một phương thức khởi dựng nạp chồng nhận một đối tượng System.Text.Encoding làm đối số (đã được mô tả trong mục 9.7). Sử dụng file nhị phân để thao tác với các kiểu dữ liệu thì khá phức tạp, vì khi truy xuất thông tin, bạn phải sử dụng một trong những phương thức Read kiểu mạnh của BinaryReader . Ví dụ: muốn truy xuất dữ liệu dạng thập phân thì phải sử dụng ReadDecimal ; còn muốn đọc một chuỗi thì phải sử dụng ReadString ( BinaryWriter luôn ghi lại chiều dài của chuỗi khi ghi chuỗi vào file để tránh lỗi). Ứng dụng Console dưới đây minh họa việc ghi và đọc một file nhị phân: using System; using System.IO; public class BinaryFileTest { private static void Main() { // Tạo file và tạo writer. FileStream fs = new FileStream("test.txt", FileMode.Create); BinaryWriter w = new BinaryWriter(fs); // Ghi một số thập phân, hai chuỗi, và một ký tự. w.Write(124.23M); w.Write("Test string"); 366 Chương 9: File, thư mục, và I/O w.Write("Test string 2"); w.Write('!'); // Bảo đảm tất cả dữ liệu được ghi từ buffer. w.Flush(); // Đóng file. w.Close(); fs.Close(); Console.WriteLine("Press Enter to read the information."); Console.ReadLine(); // Mở file trong chế độ chỉ-đọc. fs = new FileStream("test.txt", FileMode.Open); // Hiển thị dữ liệu thô trong file. StreamReader sr = new StreamReader(fs); Console.WriteLine(sr.ReadToEnd()); Console.WriteLine(); // Đọc dữ liệu và chuyển nó về kiểu thích hợp. fs.Position = 0; BinaryReader br = new BinaryReader(fs); Console.WriteLine(br.ReadDecimal()); Console.WriteLine(br.ReadString()); Console.WriteLine(br.ReadString()); Console.WriteLine(br.ReadChar()); fs.Close(); Console.ReadLine(); } } 367 Chương 9: File, thư mục, và I/O 9. 9. Đ c file m t cách b t đ ng bọ ộ ấ ồ ộ Đ c file m t cách b t đ ng bọ ộ ấ ồ ộ Bạn cần đọc dữ liệu từ một file mà không phải dừng quá trình thực thi mã lệnh của bạn. Kỹ thuật này thường được sử dụng khi file được lưu trữ trong một nơi có tốc độ truy xuất chậm (chẳng hạn một đĩa mạng). Tạo một lớp để đọc file một cách bất đồng bộ. Bắt đầu đọc một khối dữ liệu bằng phương thức FileStream.BeginRead , và truyền phương thức callback. Khi callback được kích hoạt, gọi FileStream.EndRead để truy xuất dữ liệu, xử lý nó, và đọc khối dữ liệu kế tiếp với BeginRead . FileStream hỗ trợ hoạt động bất đồng bộ thông qua phương thức BeginRead và EndRead . Sử dụng các phương thức này, bạn có thể đọc một khối dữ liệu trên một trong các tiểu trình do thread-pool cung cấp mà không cần sử dụng trực tiếp các lớp tiểu trình trong không gian tên System.Threading . Khi đọc file một cách bất đồng bộ, bạn cần xác định kích thước khối dữ liệu trong một lần đọc. Tùy trường hợp, bạn có thể muốn đọc một khối dữ liệu nhỏ (ví dụ, chép từng khối một sang file khác) hoặc khối dữ liệu tương đối lớn (ví dụ, bạn cần một lượng thông tin nhất định trước khi xử lý việc gì đó). Bạn chỉ định kích thước khối khi gọi BeginRead , và truyền một bộ đệm để chứa dữ liệu. Vì BeginRead và EndRead cần truy xuất nhiều mẩu thông tin giống nhau (chẳng hạn FileStream , bộ đệm, kích thước khối, ), bạn nên đóng gói mã lệnh đọc file bất đồng bộ trong một lớp. Với lớp AsyncProcessor trong ví dụ dưới đây, phương thức công khai StartProcess bắt đầu quá trình đọc bất đồng bộ. Mỗi khi quá trình đọc hoàn tất, OnCompletedRead được kích hoạt và khối dữ liệu được xử lý. Nếu còn dữ liệu trong file, một quá trình đọc bất đồng bộ mới sẽ được khởi chạy. Kích thước khối bộ nhớ là 2 KB (2048 byte). using System; using System.IO; using System.Threading; public class AsyncProcessor { private Stream inputStream; // Kích thước mỗi khối dữ liệu là 2 KB. private int bufferSize = 2048; public int BufferSize { get {return bufferSize;} set {bufferSize = value;} } 368 Chương 9: File, thư mục, và I/O // Bộ đệm chứa dữ liệu. private byte[] buffer; public AsyncProcessor(string fileName) { buffer = new byte[bufferSize]; // Mở file, truyền giá trị true để hỗ trợ truy xuất bất đồng bộ. inputStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, true); } public void StartProcess() { // Bắt đầu quá trình đọc bất đồng bộ. inputStream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(OnCompletedRead), null); } private void OnCompletedRead(IAsyncResult asyncResult) { // Một khối đã được đọc. Truy xuất dữ liệu. int bytesRead = inputStream.EndRead(asyncResult); // Nếu không đọc được byte nào, stream đang ở cuối file. if (bytesRead > 0) { // Tạm dừng để giả lập việc xử lý dữ liệu. Console.WriteLine("\t[ASYNC READER]: Read one block."); Thread.Sleep(TimeSpan.FromMilliseconds(20)); // Bắt đầu đọc khối kế tiếp. inputStream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(OnCompletedRead), null); }else { 369 Chương 9: File, thư mục, và I/O // Kết thúc. Console.WriteLine("\t[ASYNC READER]: Complete."); inputStream.Close(); } } } Ví dụ dưới sử dụng AsyncProcessor để đọc một file có kích thước 1 MB. public class AsynchronousIO { public static void Main() { // Tạo file thử nghiệm có kích thước 1MB. FileStream fs = new FileStream("test.txt", FileMode.Create); fs.SetLength(1000000); fs.Close(); // Bắt đầu xử lý bất đồng bộ trên một tiểu trình khác. AsyncProcessor asyncIO = new AsyncProcessor("test.txt"); asyncIO.StartProcess(); // Cùng thời điểm này, thực hiện công việc khác. // Ở đây chúng ta sẽ lặp trong 10 giây. DateTime startTime = DateTime.Now; while (DateTime.Now.Subtract(startTime).TotalSeconds < 10) { Console.WriteLine("[MAIN THREAD]: Doing some work."); // Tạm dừng để giả lập một thao tác tốn nhiều thời gian. Thread.Sleep(TimeSpan.FromMilliseconds(100)); } Console.WriteLine("[MAIN THREAD]: Complete."); Console.ReadLine(); // Xóa file thử. File.Delete("test.txt"); } 370 Chương 9: File, thư mục, và I/O } Và đây là kết xuất khi chạy ứng dụng thử nghiệm: [MAIN THREAD]: Doing some work. [ASYNC READER]: Read one block. [ASYNC READER]: Read one block. [MAIN THREAD]: Doing some work. [ASYNC READER]: Read one block. [ASYNC READER]: Read one block. [ASYNC READER]: Read one block. [ASYNC READER]: Read one block. [MAIN THREAD]: Doing some work. [ASYNC READER]: Read one block. [ASYNC READER]: Read one block. [ASYNC READER]: Read one block. . . . 10. 10. Tìm file phù h p m t bi u th c wildcardợ ộ ể ứ Tìm file phù h p m t bi u th c wildcardợ ộ ể ứ Bạn cần xử lý nhiều file có điểm chung, dựa vào biểu thức lọc như *.dll hay mysheet20??.xls . Sử dụng phiên bản nạp chồng của phương thức System.IO.DirectoryInfo. GetFiles nhận một biểu thức lọc và trả về một mảng các đối tượng FileInfo . Các đối tượng DirectoryInfo và Directory đều cho phép dò trong thư mục hiện hành để tìm các file phù hợp với một biểu thức lọc. Các biểu thức này thường sử dụng các ký tự wildcard như ? và *. Bạn cũng có thể sử dụng kỹ thuật tương tự để lấy các thư mục phù hợp với một mẫu nhất định bằng phương thức nạp chồng DirectoryInfo.GetDirectories . Ví dụ dưới đây sẽ lấy tên của tất cả các file trong một thư mục phù hợp với một biểu thức lọc. Thư mục và biểu thức lọc được truyền qua dòng lệnh. using System; using System.IO; public class WildcardTest { private static void Main(string[] args) { if (args.Length != 2) { Console.WriteLine( . EndRead . Sử dụng các phương thức này, bạn có thể đọc một khối dữ liệu trên một trong các tiểu trình do thread-pool cung cấp mà không cần sử dụng trực tiếp các lớp tiểu trình trong không gian. bit cho các ký tự ASCII bình thường và nhiều cặp 7 bit cho các ký tự mở rộng. Kiểu mã hóa này chủ yếu dùng cho các giao thức 7 bit, chẳng hạn mail. • UTF-8 Unicode—sử dụng 8 bit cho các ký tự. thị hay nút rỗng) trong tất cả các nhánh chưa được thêm vào. Điều này cho phép bạn thêm vào các phần của cây thư mục khi chúng được hiển thị. Để sử dụng kiểu giải pháp này, bạn cần ba yếu tố sau: • Phương