שלום לכולם,
במאמר הבא נבנה ביחד אפליקציה שתנצל את היכולות של System.IO ובעיקר עבודה עם כוננים, ספריות, קבצים כיווץ ו-Isolated Storage.
הרעיון מאחורי ה-System.IO Namespace הוא לרכז את כל העבודה מול Streams ולהציע ספרייה מכובדת שמאפשרת עבודה מול ההארד-דיסק.
המאמר הזה לא עוסק ב-Streams אבל עדיין חשוב שנכיר את המושג. Stream הוא מחלקה אבסטרקטית הנמצאת בתוך System.IO וממנה יורשים כל ה-Streams בפריימוורק. הרעיון מאחורי Stream הוא לאפשר גישה סינכרונית ואסינכרונית לקריאה וכתיבה ממקורות מידע שונים ולרוב בחלקים בגודל קבוע. "בחלקים בגודל קבוע" מתייחס לכך שהקריאה\כתיבה מתבצעת באמצעות buffer בגודל קבוע של בייטים. למשל נחליט לקרוא\לכתוב כל פעם 1024 בייטים. נעבור בקצרה על ה-Streams היותר ידועים: FileStream העוטף את הגישה לקבצים, NetworkStream המקשיב וכותב לתוך סוקט כלשהו ו-MemoryStream שמייצג מידע בזכרון. בנוסף, ישנן מחלקות ה-StreamReader ו-StreamWriter שאחריות על קריאת וכתיבת Streams (בהתאמה). לרוב העבודה מול Streams מוסתרת מאתנו אבל היות ומדובר במאמר על System.IO אז ניתקל בנושא הזה לא מעט וחשוב שנכיר אותו.
נתחיל את הפרוייקט שלנו ונפתח Windows application חדש בשם WinSystemIO. בטופס נוסיף Multiline Textbox בשם tbxOutput שלתוכה נדפיס את המידע שלנו במהלך הפרוייקט ונוסיף כפתור שינקה את תיבת הטקסט. נציין שאין שום חשיבות לכך שיצרנו פרוייקט חלונאי ולא פרוייקט ASP.Net היות ו-System.IO פועלת בשניהם אותו דבר.
חלק א': כוננים (או: "מישהו זוכר למה חיכינו לדוט נט 2.0 עד שהוספנו את זה?")
כעת נכיר מחלקה חדשה - System.IO.DriveInfo. המחלקה הזו היא מחלקה קונקרטית המכילה את המידע על כל כונן וכונן במחשב (וגם מאפשרת לשנות חלק מהמידע הזה). בואו נוסיף לאפליקציה שלנו את האפשרות להציג את האות שמייצגת את הכונן. נשתמש במתודה הסטטית DriveInfo.GetDrives אשר תחזיר מערך של DriveInfo לכל הכוננים במחשב ובמאפיין (גם באנגלית: Property) הנקרא DriveInfo.Name שיאפשר לנו לקבל מכל DriveInfo את האות שמייצגת את הכונן. נוסיף לאפליקציה שלנו כפתור חדש שיבצע את פעולה זו וכך יראה הקוד שלו:
private void AddNewRow(string TextToAdd)
{
tbxOutput.Text += TextToAdd + "\r\n";
}
private void btnGetAllDriveLetters_Click(object sender, EventArgs e)
{
DriveInfo[] AllDrivesOnComputer = DriveInfo.GetDrives();
foreach (DriveInfo curDriveInfo in AllDrivesOnComputer)
AddNewRow(curDriveInfo.Name);
}
הקוד יחסית פשוט - השגנו מערך שמכיל את המידע על כל הכוננים שלנו והוספנו לתיבת הטקסט את האותיות שמייצגות את כל הכוננים.
שימו לב שהשתמנו במתודה AddNewRow שיצרנו שמטרתה היא להוסיף שורה חדשה לתיבת הטקסט. נוסיף להשתמש במתודה זו אז שימו לב לכך.
נראה את התוצאה של לחיצה על btnGetAllDriveLetters:
אני יודע מה אתם חושבים ואתם כנראה צודקים - "ג'סטין, יש לך יותר מדי כוננים!".
בואו נעבור על כל התכונות שיש ל-DriveInfo:
-
Name - האות באנגלית שמייצגת את הכונן.
-
IsReady - ערך בוליאני שמחזיר האם הכונן "מוכן". ב"מוכן" הכוונה היא כאשר מדובר בכונן של מדיה נשלפת על האם יש מדיה בכונן. למשל, האם בכונן תקליטורים יש תקליטור בכונן והאם בכונן דיסקטים יש דיסקט בכונן.
-
VolumeLabel - השם שנתנו לכונן. למשל השם של ההארד-דיסק או שם הקומפילציה של התקליטור.
-
DriveFormat - מחרוזת שמייצגת את שיטת הכתיבה על הדיסק, למשל: FAT, FAT32 ו-NTFS.
-
DriveType - ערך Enum שמחזיר את סוג הכונן.
-
TotalSize - סה"כ הגודל של כונן בביטים. אם נרצה לקבל את הכמות ב-kb, mb או gb נצטרך לחלק את TotalSize ב-2 בחזקת המספר המתאים.
-
AvailableFreeSpace/TotalFreeSpace - למעשה מדובר על שתי תכונות נפרדות שמחזירות את המקום הפנוי על הכונן בביטים. יש הבדל ביניהן, אבל הוא יחסית זניח. TotalFreeSpace מחזיר את המקום שבאמת פנוי על הכונן, ו-AvailableFreeSpace מתייחס לכמות הביטים שלמשתמש הספציפי הזה יש הרשאה לכתוב על הכונן. אפשרי שערכי התכונות האלו יהיה שונה, למשל במצב של כונן רשת עם 40gb פנויים אך למשתמש המחובר לרשת יש מכסה למקסימום עד 1gb על הכונן. אבל בעקרון ערכי התכונות האלו לרוב שווים.
חשוב מאוד שנבדוק את ערך ה-IsReadey לפני שננסה להגיע לשאר ערכי ה-DriveInfo, היות ואם למשל אין תקליטור בכונן התקליטורים בטח שלא נוכל לבדוק מה שם התקליטור, כמה מקום הוא סה"כ וכמה מקום פנוי יש עליו. אם בכל זאת ננסה לקרוא למשל כל תכונה של כונן שאינו מוכן נקבל שגיאת זמן ריצה.
טוב, בוא ננצל את כל הידע החדש שהרווחנו כרגע ונדפיס את כל מה שאנחנו יודעים על כל הכוננים.
private void btnGetAllDriveInformation_Click(object sender, EventArgs e)
{
DriveInfo[] AllDrivesOnComputer = DriveInfo.GetDrives();
foreach (DriveInfo curDriveInfo in AllDrivesOnComputer)
{
AddNewRow("***" + curDriveInfo.Name + "***");
AddNewRow("-------------");
AddNewRow("Drive Letter: " + curDriveInfo.Name);
AddNewRow("Drive Type: " + curDriveInfo.DriveType.ToString());
AddNewRow("Is Drive Ready: " + curDriveInfo.IsReady);
if (curDriveInfo.IsReady)
{
AddNewRow("Drive Label: " + curDriveInfo.VolumeLabel);
AddNewRow("Drive Format: " + curDriveInfo.DriveFormat);
AddNewRow("Total Size: " + curDriveInfo.TotalSize);
AddNewRow("Available Free Space: " + curDriveInfo.AvailableFreeSpace);
AddNewRow("Total Free space: " + curDriveInfo.TotalFreeSpace);
}
AddNewRow("-------------");
AddNewRow("");
}
}
החזרנו מערך של כל ה-DriveInfo שיש במחשב, נדפיס את האות שמייצגת את הכונן ואת סוג הכונן. עכשיו נבדוק אם הכונן מוכן. אם הכונן לא מוכן - לא נוכל לבדוק את כל התכונות בתוך התנאי. אם הכונן כן מוכן נדפיס את ערכי התכונות האלו. נעבור להדפיס את הכונן הבא. בוא נראה את התוצאה:
ניתן לראות שכונן E הוא כונן תקליטורים שאין בו תקליטור בכונן, ולכן לא ניסינו להדפיס את שאר הפרטים. אם היינו מנסים, היינו מקבלים שגיאת IOException עם ההודעה "The Device is not ready".
עכשיו נראה איך להשיג אינפורמציה על כונן שהאות המייצגת אותו ידועה. נוכל להשתמש בקונסטרקטור של DriveInfo ולתת לו את אות הכונן.
private void btnGetCDriveInformation_Click(object sender, EventArgs e)
{
DriveInfo curDriveInfo = new DriveInfo("C:");
AddNewRow("***" + curDriveInfo.Name + "***");
AddNewRow("-------------");
AddNewRow("Drive Letter: " + curDriveInfo.Name);
AddNewRow("Drive Type: " + curDriveInfo.DriveType.ToString());
AddNewRow("Is Drive Ready: " + curDriveInfo.IsReady);
if (curDriveInfo.IsReady)
{
AddNewRow("Drive Label: " + curDriveInfo.VolumeLabel);
AddNewRow("Drive Format: " + curDriveInfo.DriveFormat);
AddNewRow("Total Size: " + curDriveInfo.TotalSize);
AddNewRow("Available Free Space: " + curDriveInfo.AvailableFreeSpace);
AddNewRow("Total Free space: " + curDriveInfo.TotalFreeSpace);
}
AddNewRow("-------------");
AddNewRow("");
}
ותוצאות ההרצה:
חלק ב': תיקיות (או: "פן וטלר, קונקרטי וסטטי")
חשוב שנעמוד על ההבדל בין מחלקות סטטיות למחלקות קונקרטיות בחלק זה של הפריימוורק. החלקים הבאים יעסקו בתיקיות וקבצים בכוננים על המחשב. לכל אחד מחלקי System.IO האלו יש מחלקה סטטית המכילה מתודות סטטיות ובנוסף קיימות מחלקות קונטקרטיות המכילות מתודות ומאפיינים. ובפרט, יש את System.IO.Directory המכילה מתודות סטטיות לתיקיות ויש את DirectoryInfo המכילה יצוג קונקרטי של המידע על תיקייה מסויימת. בהמשך נראה שיש גם את File ו-FileInfo. חשוב שנפנים את ההבדל האמיתי בין המחלקות הסטטיות למחלקות הקונקרטיות - המחלקות הסטטיות הן מחלקות סטטיות והמחלקות הקונקרטיות הן מחלקות קונקרטיות. תקראו את המשפט הקודם שוב.
אין הבדל דרסטי ביכולות לבצע פעולה מסויימת ו\או להשיג מידע מסויים בין המחלקות הסטטיות לבין המחלקות הקונקרטיות. מרבית המתודות והאפשרויות קיימות גם במחלקה הסטטית וגם במחלקה הקונקרטית. במאמר זה נציג עבודה גם עם המחלקות הסטטיות וגם עם המחלקות הקונקרטיות.
בואו נתחיל ונבדוק אם התיקייה c:\myDir קיימת:
private void btnDoesCmyDirExist_Static_Click(object sender, EventArgs e)
{
AddNewRow(Directory.Exists(@"C:\myDir").ToString());
}
private void btnDoesCmyDirExist_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
AddNewRow(curDirectoryInfo.Exists.ToString());
}
במתודה הראשונה בדקנו בעזרת המתודה הסטטית Directory.Exist.
במתודה השנייה יצרנו מופע של DirectoryInfo עם כתובת הספרייה ובדקנו בעזרת המאפיין DirectoryInfo.Exist.
נראה את המסך שלנו עם התוצאות:
ראיתם מה עשינו? בדקנו גם באמצעות מתודה סטטית וגם באמצעות מתודה של המחלקה הקונקרטית. נמשיך ונעשה כך לא מעט פעמים במאמר זה.
עכשיו אחרי שראינו שהתיקייה c:\myDir לא קיימת בואו ניצור תיקייה חדשה c:\myDir:
private void btnCreateCmyDir_Static_Click(object sender, EventArgs e)
{
if (!Directory.Exists(@"C:\myDir"))
{
Directory.CreateDirectory(@"C:\myDir");
AddNewRow(@"C:\myDir Created!");
}
}
private void btnCreateCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (!curDirectoryInfo.Exists)
{
curDirectoryInfo.Create();
AddNewRow(@"C:\myDir Created!");
}
}
בפשטות, בדקנו אם התיקייה קיימת ואם היא אכן לא קיימת אז יצרנו אותה. אם ננסה ליצור תיקייה שכבר קיימת לא נקבל שום שגיאה, אבל זה עדיין חשוב שנבצע כאלו בדיקות. (הרי הגיוני שאין שום משמעות ליצירת תיקייה שכבר קיימת). בואו נראה את התוצאות:
ואכן נוצרה תיקיית C:\myDir. (תסמכו עליי)
אם אתם מכירים אותי כנראה תדעו שאחרי שיצרתי את הספריית הדגמה הזו כנראה מאוד שאני אשכח למחוק אותה. אז בואו ניצור כאן מחיקת ספרייה שיזכיר לי. נמחוק את הספרייה c:\myDir:
private void btnDeleteCmyDir_Static_Click(object sender, EventArgs e)
{
if (!Directory.Exists(@"C:\myDir"))
{
Directory.Delete(@"C:\myDir");
AddNewRow(@"C:\myDir Deleted!");
}
}
private void btnDeleteCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (!curDirectoryInfo.Exists)
{
curDirectoryInfo.Delete();
AddNewRow(@"C:\myDir Deleted!");
}
}
ונראה את התוצאות:
ואכן נמחקה הספרייה c:\myDir. (שוב פעם ,תסמכו עליי)
עכשיו בואו נשנה את שם התיקייה מ-C:\myDir ל-C:\myOtherDir:
private void btnRenameCmyDir_Static_Click(object sender, EventArgs e)
{
if (Directory.Exists(@"C:\myDir"))
{
Directory.Move(@"C:\myDir", @"C:\myOtherDir");
AddNewRow(@"C:\myDir renamed to C:\myOtherDir!");
}
}
private void btnRenameCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
curDirectoryInfo.MoveTo(@"C:\myOtherDir");
AddNewRow(@"C:\myDir renamed to C:\myOtherDir!");
}
}
ונראה את התוצאות:
ואכן שם התיקייה שונה. שימו לב למה שלמעשה עשינו זה "העברנו" את התיקייה c:\myDir לכתובת C:\myOtherDir. שמבחיתנו זה פשוט לתת שם חדש לספרייה.
עכשיו בואו נעביר את התיקייה מ-C:\myDir לתוך C:\myOtherDir:
private void btnMoveCmyDir_Static_Click(object sender, EventArgs e)
{
if (Directory.Exists(@"C:\myDir"))
{
Directory.Move(@"C:\myDir", @"C:\myOtherDir\myDir");
AddNewRow(@"C:\myDir moved into C:\myOtherDir!");
}
}
private void btnMoveCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
curDirectoryInfo.MoveTo(@"C:\myOtherDir\myDir");
AddNewRow(@"C:\myDir moved into C:\myOtherDir!");
}
}
ונראה את התוצאות:
ואכן התיקייה הועברה.
עכשיו ניצור בתוך C:\myDir את התיקייה SomeOtherDir:
private void btnCreateSomeOtherDirInCmyDir_Static_Click(object sender, EventArgs e)
{
if (Directory.Exists(@"C:\myDir"))
{
Directory.CreateDirectory(@"C:\myDir\SomeOtherDir");
AddNewRow(@"Created subdirectory C:\myDir\SomeOtherDir!");
}
}
private void btnCreateSomeOtherDirInCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
curDirectoryInfo.CreateSubdirectory(@"SomeOtherDir");
AddNewRow(@"Created subdirectory C:\myDir\SomeOtherDir!");
}
}
ונראה את התוצאות:
ואכן נוצרה התייקיה C:\myDir\SomeOtherDir.
עכשיו בואו נקבל את מערך תת-תיקיות בתוך C:\myDir ונדפיס אותן:
private void btnListSubdirectories_Static_Click(object sender, EventArgs e)
{
if (Directory.Exists(@"C:\myDir"))
{
string[] SubDirs = Directory.GetDirectories(@"C:\myDir");
foreach (string curDirInfo in SubDirs)
AddNewRow(curDirInfo + @" Is a subdir of c:\myDir");
}
}
private void btnListSubdirectories_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
DirectoryInfo[] SubDirs = curDirectoryInfo.GetDirectories();
foreach(DirectoryInfo curDirInfo in SubDirs)
AddNewRow(curDirInfo.FullName + @" Is a subdir of c:\myDir");
}
}
ונראה את התוצאות:
ואכן התת-תיקיות של c:\myDir הודפסו. שימו לב להבדל בין המתודה הסטטית Directory.GetDirectories לבין המתודה הקונקרטית DirectoryInfo.GetDirectories. המתודה הסטטית מחזירה מערך של מחרוזות המכילות את הכתובת המלאה של התיקייה, והמתודה הקונקרטית מחזירה מעריך של DirectoryInfo של התת-תיקיות.
אמרנו שכל מה שאפשר לעשות עם מתודות סטטיות של Directory אפשר לעשות עם גם עם מתודו קונקרטיות של DirectoryInfo (ולהפך). זה כל-כך נכון שגם בשביל המאפיינים (באנגלית: Properties) של DirectoryInfo יש מתודות סטטיות. למשל בשביל המאפיין DirectoryInfo.CreateTime שמייצג את תאריך ושעת יצירת התיקייה יש את המתודות Directory.GetCreateTime ו-Directory.SetCreateTime. בואו נראה דוגמה להדפסת תאריך יצירת C:\myDir:
private void btnGetPermissionsForCmyDir_Static_Click(object sender, EventArgs e)
{
if (Directory.Exists(@"C:\myDir"))
{
string CreatedDate = Directory.GetCreationTime(@"C:\myDir").ToLongDateString();
AddNewRow(@"C:\myDir was created on: " + CreatedDate);
}
}
private void btnGetPermissionsForCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
string CreatedDate = curDirectoryInfo.CreationTime.ToLongDateString();
AddNewRow(@"C:\myDir was created on: " + CreatedDate);
}
}
נביט על התוצאה:
ואכן הודפס תאריך יצירת התיקייה.
ועכשיו בואו נשנה את תאריך ושעת יצירת התיקייה C:\myDir לתאריך והשעה הנוכחיים:
private void btnChangeCreateDateForCmyDir_Static_Click(object sender, EventArgs e)
{
if (Directory.Exists(@"C:\myDir"))
{
Directory.SetCreationTime(@"C:\myDir", DateTime.Now);
AddNewRow(@"Changed C:\myDir creation time to: " + DateTime.Now);
}
}
private void btnChangeCreateDateForCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
curDirectoryInfo.CreationTime = DateTime.Now;
AddNewRow(@"Changed C:\myDir creation time to: " + DateTime.Now);
}
}
נביט את התוצאה:
ואכן תאריך ושעת יצירת התיקייה שונו.
באופן דומה ל-CreateTime יש גם את DirectoryInfo.LastAccessTime ו-DirectoryInfo.LastWriteTime יש את המתודות הסטטיות המקבילות Directory.GetLastAccessTime, Directory.SetLastAccessTime ו-Directory.GetLastWriteTime, Directory.SetLastWriteTime (בהתאמה).
נביט על שתי מאפיינים של DirectoryInfo שאין להם מקבילם במתודות סטטיות - DirectoryInfo.Attributes ו-DirectoryInfo.Parent. ה
המאפיין DirectoryInfo.Parent מאפשר לנו לקבל את הספרייה שמכילה את הספרייה הנוכחית. אם היינו רוצים לעשות זאת במופע סטטי בלי ליצור עותק קונקרטי של DirectoryInfo היינו צריכים להתחיל לשחק עם מחרוזות. נראה דוגמה להדפסת התיקייה המכילה את התיקייה C:\myDir\SomeOtherDir:
private void btnGetParentOfCmyDirSomeOtherDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir\SomeOtherDir");
if (curDirectoryInfo.Exists)
{
DirectoryInfo ParentDirectoryInfo = curDirectoryInfo.Parent;
AddNewRow(@"Parent of C:\myDir\SomeOtherDir is: " + ParentDirectoryInfo.FullName);
}
}
שימו לב ש-DirectoryInfo.Parent מחזיר לנו DirectoryInfo של התיקייה המכילה את התיקייה הנוכחית.
נביט על התוצאה:
ואכן SomeOtherDir נמצאת בתוך C:\myDir.
המאפיין DirectoryInfo.Attributes מאפשר לנו לקבוע האם תיקייה היא נסתרת, לקריאה בלבד, תיקיית מערכת וכל מיני מאפיינים נוספים מעניינים. המאפיין DirectoryInfo.Attributes הוא Enum מסוג FileAttributes. בואו נביט על הערכים האפשריים של ה-Enum:
בואו נקבע את C:\myDir כ-ReadOnly:
private void btnSetReadonlyForCmyDir_Concrete_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
curDirectoryInfo.Attributes = FileAttributes.ReadOnly;
AddNewRow(@"C:\myDir is now readonly!");
}
}
נביט על התוצאות:
ואכן C:\myDir הוא עכשיו לקריאה בלבד.
או קיי, ראינו איך ניתן לקבוע תייקיה כ-Readonly וכנראה מאוד שלקבוע את התיקייה רק כ-Hidden או System או כל דבר אחר זאת מטלה מאוד דומה. אבל בואו נביט על תמונת המסך הקודמת ונבקש כזה דבר - תיקייה שהיא גם Readonly וגם Hidden!
אנחנו הרי יודעים שDirectoryInfo.Attributes הוא Enum לכל דבר ואנחנו גם יודעים ש-Enum יכול לקבל רק ערך אחד. הרי אומרים איזה ערך אנחנו רוצים מתוך ערכי ה-Enum וזהו. זה נכון. אבל ניתן לקבוע סוג מיוחד של Enum שנקרא Flags שיכול לקבל מספר ערכים אפשריים של Enum. במקרה לחלוטין FileAttributes הוא באמת Enum מסוג Flags. נפריד את הערכים שאנו רוצים ל-Flags Enum בעזרת התו |. בואו נקבע לתיקייה c:\myDir שהיא גם Readonly וגם Hidden:
private void btnSetMultipleAttributesForCmyDir_Click(object sender, EventArgs e)
{
DirectoryInfo curDirectoryInfo = new DirectoryInfo(@"c:\myDir");
if (curDirectoryInfo.Exists)
{
curDirectoryInfo.Attributes = FileAttributes.ReadOnly
| FileAttributes.Hidden;
AddNewRow(@"C:\myDir is now readonly & Hidden!");
}
}
נביט על התוצאה:
ואכן c:\myDir היא גם קריאה בלבד וגם נסתרת.
בואו נתעקב לשנייה על כל עניין ה-Flags הזה, הרי זה מאוד מעניין. למשל, איך בדיוק אני אומר ש-Enum שלי הוא Flags? ומה המנגנון מאחורי זה שמאפשר לי לבחור מספר ערכים ב-Enum?
בשביל לענות על זה בואו נביט על ההגדרה של FileAttributes.
נתחיל מזה שאנחנו רואים שמדובר public enum. אבל מעל ה-Enum יש את [Flags]. זאת ה-Attribute שמאפשרת לנו מבחינת קומפליציה באמת לבחור כמה ערכים של ה-Enum. אם לא היה את ה-FlagsAttribute מעל ה-Enum לא היינו יכולים לבחור מספר ערכים.
בואו נשים לב לעוד דבר מעניין - ערכי ה-Enum הם כולם 2 בחזקת i. הערך הנומרי שמייצג את Readonly הוא 2 בחזקת 0, הערך הנומרי שמייצג את hidden הוא 2 בחזקת 1, הערך הנומרי שמייצג את System הוא 2 בחזקת 2 וכך הלאה. אבל רגע, למה? בואו נגיד שהערך של איזהשהו הוא DirectoryInfo.Attributes הוא 1, אז ברור שהתיקייה היא Readonly. בואו ונגיד שהערך הוא 2 אז התקייה היא Hidden. בואו נגיד שהערך הוא 3 אז התיקיה Readonly וגם Hidden. אבל ל-System היה את הערך 3 היינו כאן בבעיה, כי DirectoryInfo.Attributes של 3 לא היה יכול להצביע ל-Readonly&Hidden. מה מסתבר? ה-2 בחזקת i מאפשר מספיק "רווחים" בין ערכי ה-enum כך שכל צירוף מספרי שנבחר בהכרח יהיה יצוג לכל היותר לסט אחד ויחיד של ערכי ה-enum.
אז נרצה מתישהו להוסיף Flags לאפליקציה שלנו נכתוב enum רגיל, נוסיף את [Flags] ונקבע את הערכים הנומריים של ה-enum כ-2 בחזקת i.
נסכם את ההסבר על Flags בציטוט של מומחה דוט-נט מיקי ווטס (קישור לבלוג שלו בצד שמאל):
"מכיוון שאנחנו מדברים פה למעשה על דגלים, שלכל אחד יש ערך עם ייצוג בינארי של ביט אחד דלוק וכל השאר מכובים, ניתן למעשה לשלב את הדגלים ע"י הדלקת הביטים הרלבנטיים ע"י שימוש בפעולות מתמטיות בסיסיות. במקרה ויש Enum, ניתן לתת לו Attribute בשם Flags, מה שמאפשר להתייחס אליו כאל שילוב של דגלים."
חלק ג': קבצים (או: "בדיוק כמו קודם, רק עם המילה File במקום Directory ועכשיו יש תוכן")
כמו שעולה מן הכותרת - עכשיו נעשה את כל מה שעשינו קודם עם תיקיות רק עם קבצים. בשביל זה יש לנו את המחלקה הסטטית File ואת המחלקה הקונקרטית FileInfo. נעשה כמה דוגמאות של פעולות בסיסיות וניתן קוד לדוגמה בשימוש גם במחלקה הקונקרטית וגם במחלקה הסטטית. אחרי הכמה דוגמאות הבסיסיות הללו נעבור לקריאה מקבצים וכתיבה לקבצים.
בדיקה האם קיים הקובץ C:\myDir\myFile.txt:
private void btnDoesmyFileExist_Static_Click(object sender, EventArgs e)
{
bool DoesFileExist = File.Exists(@"C:\myDir\myFile.txt");
AddNewRow(@"Does C:\myDir\myFile.txt exists? " + DoesFileExist.ToString());
}
private void btnDoesmyFileExist_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
bool DoesFileExist = curFileInfo.Exists;
AddNewRow(@"Does C:\myDir\myFile.txt exists? " + DoesFileExist.ToString());
}
יצירת הקובץ C:\myDir\myFile.txt: private void btnCreatemyFile_Static_Click(object sender, EventArgs e)
{
if (!File.Exists(@"C:\myDir\myFile.txt"))
{
File.Create(@"C:\myDir\myFile.txt");
AddNewRow(@"C:\myDir\myFile.txt Created!");
}
}
private void btnCreatemyFile_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
if (!curFileInfo.Exists)
{
curFileInfo.Create();
AddNewRow(@"C:\myDir\myFile.txt Created!");
}
}
מחיקת הקובץ C:\myDir\myFile.txt:
private void btnDeletemyFile_Static_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
File.Delete(@"C:\myDir\myFile.txt");
AddNewRow(@"C:\myDir\myFile.txt deleted!");
}
}
private void btnDeletemyFile_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
if (curFileInfo.Exists)
{
curFileInfo.Delete();
AddNewRow(@"C:\myDir\myFile.txt deleted!");
}
}
שינוי שם הקובץ C:\myDir\myFile.txt ל-myOtherFile.txt:
private void btnRenamemyFileTomyOtherFile_Static_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
File.Move(@"C:\myDir\myFile.txt", @"C:\myDir\myOtherFile.txt");
AddNewRow(@"C:\myDir\myFile.txt was renamed to myOtherFile.txt!");
}
}
private void btnRenamemyFileTomyOtherFile_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
if (curFileInfo.Exists)
{
curFileInfo.MoveTo(@"C:\myDir\myOtherFile.txt");
AddNewRow(@"C:\myDir\myFile.txt was renamed to myOtherFile.txt!");
}
}
העברת הקובץ myFile.Txt מהתיקייה C:\myDir לתיקייה C:\myOtherDir:
private void btnMovemyFileTomyOtherDir_Static_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
File.Move(@"C:\myDir\myFile.txt", @"C:\myOtherDir\myFile.txt");
AddNewRow(@"C:\myDir\myFile.txt was moved to c:\myOtherDir!");
}
}
private void btnMovemyFileTomyOtherDir_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
if (curFileInfo.Exists)
{
curFileInfo.MoveTo(@"C:\myOtherDir\myFile.txt");
AddNewRow(@"C:\myDir\myFile.txt was moved to c:\myOtherDir!");
}
}
מציאת תאריך יצירת הקובץ C:\myDir\myFile.txt:
private void btnGetmyFileCreateTime_Static_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
string TimeFileCreated = File.GetCreationTime(@"C:\myDir\myFile.txt").ToLongTimeString();
AddNewRow(@"C:\myDir\myFile.txt was created on: " + TimeFileCreated);
}
}
private void btnGetmyFileCreateTime_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
if (curFileInfo.Exists)
{
string DateTimeFileCreated = curFileInfo.CreationTime.ToLongTimeString();
AddNewRow(@"C:\myDir\myFile.txt was created on: " + DateTimeFileCreated);
}
}
שינוי תאריך יצירת הקובץ C:\myDir\myFile.txt:
private void btnSetmyFileCreateTimeToNow_Static_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
File.SetCreationTime(@"C:\myDir\myFile.txt", DateTime.Now);
AddNewRow(@"C:\myDir\myFile.txt creation date was changed to: "
+ DateTime.Now.ToLongTimeString());
}
}
private void btnSetmyFileCreateTimeToNow_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
if (curFileInfo.Exists)
{
curFileInfo.CreationTime = DateTime.Now;
AddNewRow(@"C:\myDir\myFile.txt creation date was changed to: "
+ DateTime.Now.ToLongTimeString());
}
}
כמו לתיקיות, במקביל ל-CreateTime יש גם את FileInfo.LastAccessTime ו-FileInfo.LastWriteTime יש את המתודות הסטטיות המקבילות File.GetLastAccessTime, File.SetLastAccessTime ו-File.GetLastWriteTime, File.SetLastWriteTime (בהתאמה).
קביעת הקובץ C:\myDir\myFile.txt כקריאה בלבד ונסתר:
private void btnMakemyFileHiddenAndReadonly_Static_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
FileAttributes HiddenAndReadOnly = FileAttributes.ReadOnly
| FileAttributes.Hidden;
File.SetAttributes(@"C:\myDir\myFile.txt", HiddenAndReadOnly);
AddNewRow(@"C:\myDir\myFile.txt is now Readonly & Hidden!");
}
}
private void btnMakemyFileHiddenAndReadonly_Concrete_Click(object sender, EventArgs e)
{
FileInfo curFileInfo = new FileInfo(@"C:\myDir\myFile.txt");
if (curFileInfo.Exists)
{
curFileInfo.Attributes = FileAttributes.ReadOnly
| FileAttributes.Hidden;
AddNewRow(@"C:\myDir\myFile.txt is now Readonly & Hidden!");
}
}
חשוב לשים לב שעבדנו עם תיקיות לא הייתה קיימת המתודה הסטטית Directory.SetAttributes ולכן אין לנו את האפשרות לשנות את ה-FileAttributes של תיקייה באופן סטטי. בקבצים כן קיימת האפשרות לשנות את ה-Attributes של קבצים באופן סטטי בעזרת File.SetAttributes.
בתחילת מאמר זה ציינתי כי לא ניגע בנושא ה-Streamים. רוב העבודה בקריאה וכתיבה לקבצים מתבצעת בעזרת Streamים ולכן לא נראה יותר מדי תוכן. כאן נדגים את העבודה עם המתודות הסטטיות של File המאפשרות כתיבה וקריאה מקבצים בבת-אחת. כלומר, אנחנו או נקרא את כל הקובת, או נכתוב את כל הקובץ (או נוסיף שורה בודדה לסוף הקובץ). בעבודה עם StreamReader ו-StreamWriter היינו יכולים לכתוב מספר שורות באותה גישה לכונן, היינו יכולים לבצע קריאה וכתיבה בו זמנית וכך הלאה. אבל כמו שאמרתי, כרגע היקף המאמר מונע מלעסוק בנושא מסדר גודל זה. תדעו שהוא קיים ואם באמת תצטרכו מתישהו לעבוד אינטנסיבית עם הרבה קבצים או קבצים מאוד גדולים עדיף שתחקרו קודם איך לעבוד עם Streamים.
כתיבת Hello World לתוך הקובץ C:\myDir\myFile.txt:
נשתמש במתודת File.WriteAllText בכדי לאתחל קובץ ולכתוב לתוכו. במידה והקובץ אינו קיים יווצר קובץ בעל השם הקובץ הזה. במידה והקובץ כן קיים תוכן הקובץ ימחק לחלוטין. כך שבכל מצב, בתוך הקובץ יהיה רק הטקסט שאנו כותבים לתוכו כעת.
חשוב לציין שאם היינו רוצים לכתוב כמות מאוד גדולה של טקסט (למשל את כל שירי אלתרמן ביחד) עדיף בחום להשתמש ב-Streamים.
private void btnClearmyFileAndWriteHellomyWorld_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
File.WriteAllText(@"C:\myDir\myFile.txt", "Hello world!");
AddNewRow(@"C:\myDir\myFile.txt now contains only Hello world.");
}
}
הוספת Hello World לסוף הקובץ C:\myDir\myFile.txt:
בדוגמה הקודמת שכתבנו לתוך קובץ קודם דאגנו לאתחל אותו. המתודה File.AppendAllText מאפשרת לנו להוסיף לסוף קובץ קיים טקסט.
private void btnAppendHelloWorldTomyFile_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
File.AppendAllText(@"C:\myDir\myFile.txt", "Hello world!");
AddNewRow(@"Hello world was appended to C:\myDir\myFile.txt.");
}
}
קריאת תוכן הקובץ C:\myDir\myFile.txt לתוך מחרוזת:
נשתמש במתודה File.ReadAllText לקרוא את תוכן כל הקובץ לתוך מחרוזת.
private void btnPrintAllTextInFile_Click(object sender, EventArgs e)
{
if (File.Exists(@"C:\myDir\myFile.txt"))
{
string FileContents = File.ReadAllText(@"C:\myDir\myFile.txt");
AddNewRow(@"-----C:\myDir\myFile.txt content:----");
AddNewRow(FileContents);
}
חלק ד': תרגילים מתקדמים בכוננים, תיקיות וקבצים (או: "הקשר בין כוננים, תיקיות וקבצים")
עד עכשיו עבדנו עם כוננים בנפרד, ועם תיקיות בנפרד ועם קבצים בנפרד. עכשיו בואו נתפור על הדברים הללו ביחד לכדי חווית ניווט אחת. בכל הדוגמאות האלו נעבוד עם DriveInfo, DirectoryInfo ו-FileInfo היות וכל המתודות הרלוונטיות שנעבוד איתן מחזירות רק טיפוסים כאלו.
נתחיל מלהוציא רשימת תיקיות בכונן C. נשתמש ב-DriveInfo.RootDirectory כדי לגשת לספרייה הראשית של הכונן (נעבור מלעבוד עם כוננים לעבודה עם תיקיות), ונבקש מה-DriveInfo.RootDirectory שתחזיר לנו את רשימת התיקיות בה.
private void btnListDirectoriesInCdrive_Click(object sender, EventArgs e)
{
DriveInfo Cdrive = new DriveInfo("C:");
AddNewRow(@"*** All Directories in C:\ ***");
DirectoryInfo[] AllDirectoriesInCDrive = Cdrive.RootDirectory.GetDirectories();
foreach (DirectoryInfo curDirectoryInfo in AllDirectoriesInCDrive)
{
AddNewRow(curDirectoryInfo.FullName + " (" + curDirectoryInfo.Attributes.ToString() + ")");
}
}
נביט על מה עשינו. דבר ראשון איתחלנו DriveInfo של כונן C (כמו שראינו בחלק א' של מאמר זה). דבר שני, ניגשנו לתיקיית השורש של הכונן. אחר כך, ביקשנו רשימת תת-תיקיות בתוך תיקיית השורש באמצעות Cdrive.RootDirectory.GetDirectories. עברנו בלולאה על כל התיקיות וביקשנו שידפיס את הנתיב המלא של התיקיות האלו וה-Attributes שלהן. בסוף נקבל רשימה של תיקיות בתוך כונן C עם ה-Attributes שלהן. בואו נראה את התוצאה:
אפשר לראות את התיקייה c:\Program Files שהיא ספרייה לקריאה בלבד. אפשר לראות את C:\System Volume Information שהיא ספריית מערכת נסתרת.
אחרי שהשגנו רשימת תיקיות בכונן C נרצה עכשיו להשיג רשימת קבצים בכונן C. הפעם במקום לקבל מערך של DirectoryInfo נקבל מערך של FileInfo ובמקום להשתמש במתודה GetDirectories נשתמש במתודה GetFiles.
private void btnListFilesOnCDrive_Click(object sender, EventArgs e)
{
DriveInfo Cdrive = new DriveInfo("C:");
AddNewRow(@"*** All Files in C:\ ***");
FileInfo[] AllFilesInCDrive = Cdrive.RootDirectory.GetFiles();
foreach (FileInfo curFileInfo in AllFilesInCDrive)
{
AddNewRow(curFileInfo.FullName + " (" + curFileInfo.Attributes.ToString() + ")");
}
}
בדיוק כמו קודם רק שכאן אנו עובדים עם מתודת GetFiles ולא עם GetDirectories ובלולאה אנו עוברים על מערך של FileInfo.
בואו נראה את התוצאה של ההרצה הזו:
ובאמת כל הקבצים עם ה-Attributes שלהם הודפסו.
ואני יודע מה אתם חושבים - "ג'סטין, יש לך יותר מדי קבצים" ואתם צודקים לחלוטין. אז בואו נציג רק את הקבצים בכונן C עם סיומת Txt. מסתבר שיש אפשרות לשלוח למתודה GetFiles תבנית חיפוש לאיזה קבצים אנחנו רוצים. אז בואו נראה איך הקוד יראה כעת:
private void btnTxtFilesOnCDrive_Click(object sender, EventArgs e)
{
DriveInfo Cdrive = new DriveInfo("C:");
AddNewRow(@"*** All Txt files in C:\ ***");
FileInfo[] AllFilesInCDrive = Cdrive.RootDirectory.GetFiles("*.txt");
foreach (FileInfo curFileInfo in AllFilesInCDrive)
{
AddNewRow(curFileInfo.FullName + " (" + curFileInfo.Attributes.ToString() + ")");
}
}
השינוי מאוד פשוט - הוספנו למתודה GetFiles את תבנית החיפוש שלנו. אגב, ניתן גם להזין תבנית חיפוש ל-GetDirectories.
בואו נראה את התוצאה:
ואכן כל קבצי הטקסט בכונן C מופיעים כאן.
בואו ניקח את הדוגמה עוד צעד קדימה: נרצה להדפיס את כל קבצי הטקסט בכל כונן C. כלומר, לא רק בתיקיית השורש, אלא ממש בכל הכונן. ושוב השינוי יהיה אחד מאוד קטן. למתודה GetFiles אפשר גם להגדיר שהחיפוש הוא רקורסיבי בתוך כל התת-תיקיות והתת-תיקיות שלהן וכך הלאה. כברירת מחדל החיפוש אינו רקורסיבי, אבל בדוגמה הזו נגיד לו שאנו כן רוצים חיפוש רקורסיבי:
private void btnPrintAllTextFilesOnCdrive_Click(object sender, EventArgs e)
{
DriveInfo Cdrive = new DriveInfo("C:");
AddNewRow(@"*** All Txt files in C:\ ***");
FileInfo[] AllFilesInCDrive = Cdrive.RootDirectory.GetFiles("*.txt", SearchOption.AllDirectories);
foreach (FileInfo curFileInfo in AllFilesInCDrive)
{
AddNewRow(curFileInfo.FullName + " (" + curFileInfo.Attributes.ToString() + ")");
}
}
סה"כ הוספנו ברירה שאנו רוצים כי החיפוש יהיה רקורסיבי בכל התת-תיקיות. (למי שתוהה, אין עוד ערכים מלבד TopDirectory ו-AllDirectories ל-SearchOption). בואו נראה את התוצאה:
אגב, רוב הסיכויים שאם תריצו את זה אצלכם זה יהיה מאוד איטי (היות ומדובר בחיפוש רקרוסיבי בכל כונן C) וגם יתכן כי החיפוש יכשל עקב בעיית הרשאות בתוך כונני המערכת (למשל גם משתמש Administrator לא יכול להריץ חיפוש בתוך תיקיית System Volume Information).
עכשיו בואו נעשה דוגמה להדפסת כל הקבצים בתוך תיקייה (בדוגמאות הקודמות הדפסנו מתיקיית השורש של כוננים). זה יהיה מאוד דומה לדוגמאות הקודמות רק שכאן נאתחל DirectoryInfo לתיקייה c:\myDir ובתוכה נריץ את GetFiles. בנוסף, נרצה שבמקום להדפיס את ה-Attributes של הקבצים, נדפיס את הגודל שלהם בקילו-בייטס.
private void btnFilesInCmyDir_Click(object sender, EventArgs e)
{
DirectoryInfo CmyDir = new DirectoryInfo(@"C:\myDir");
FileInfo[] AllFilesInCmyDir = CmyDir.GetFiles();
foreach (FileInfo curFileInfo in AllFilesInCmyDir)
{
AddNewRow(curFileInfo.Name + " (" + curFileInfo.Length / 1024 + "kb)");
}
}
איתחלנו DirectoryInfo לתיקייה C:\myDir, החזרנו את הקבצים בתוכה, עברנו עליהם בלולאה והדפסנו את השם שלהם והגודל.
שימו לב שהדפסנו FileInfo.Name ולא FileInfo.FullName ככה שהדפסנו את שם הקובץ בלי הנתיב המלא. בנוסף לקחנו את הגודל בבייטים וחיקלנו ב-1024 כדי לקבל את גודל הקובץ בקילו בייטים. נראה את התוצאה:
עכשיו בואו נעשה דוגמה אחת מסכמת. נרצה להעתיק ספרייה עם כל התת-ספריות והקבצים שבתוכם. באופן מפתיע, אין כזו מתודה בפריימוורק. היינו בקלות יכולים לשבת לכתוב מתודה כזו משלנו. היינו יוצרים מתודה רקורסיבית שעוברת על תיקיות, יוצרת את התיקייה הנוכחית במקום החדש, מעתיקה את כל הקבצים שלה, יוצרת את התת-תיקיות שלה וקוראת למתודה עצמה ברקורסיה לתת-תיקיות.
אבל הייתי מעדיף להביט על קוד של מישהו אחר שכבר עשה את זה בשבילנו. למזלנו, ריצד'רד לופס יש וכתב כזו מתודה. בואו נראה מה הוא כתב, נבין מה הוא עשה ונשתמש במתודה שלו:
// taken from - http://www.codeproject.com/csharp/copydirectoriesrecursive.asp
public void copyDirectory(string Src, string Dst)
{
String[] Files;
if (Dst[Dst.Length - 1] != Path.DirectorySeparatorChar)
Dst += Path.DirectorySeparatorChar;
if (!Directory.Exists(Dst)) Directory.CreateDirectory(Dst);
Files = Directory.GetFileSystemEntries(Src);
foreach (string Element in Files)
{
// Sub directories
if (Directory.Exists(Element))
copyDirectory(Element, Dst + Path.GetFileName(Element));
// Files in directory
else
File.Copy(Element, Dst + Path.GetFileName(Element), true);
}
}
נתחיל מהתחלה, נראה את המתודה copyDirectory. אפשר לראות כי בהמשך המתודה קוראת לעצמה ולכן מדובר בבירור במתודה רקורסיבית. המתודה מקבלת נתיב נוכחי שממנו אנו מעתיקים ונתיב נוכחי אליו אנו מעתיקים.
נביט על השתי שורות קוד הראשונות:
if (Dst[Dst.Length - 1] != Path.DirectorySeparatorChar)
Dst += Path.DirectorySeparatorChar;
בשורות האלו אנו בודקים כי ליעד שלנו יש סימן "\" בסוף הכתובת, ואם לא נוסיף אחד כזה בסוף היעד. הקבוע Path.DirectorySeperatorChar ניגש לפרימוורק ומציא את התו המפריד בין תיקיות במערכת ההפעלה שלנו, קרי "\".
נביט על השורה הבאה:
if (!Directory.Exists(Dst)) Directory.CreateDirectory(Dst);
כאן אנו בודקים אם תיקיית היעד קיימת, ואם לא אז ניצור אותה.
נביט על השורה הבאה:
Files = Directory.GetFileSystemEntries(Src);
המתודה Directory.GetFileSystemEntries מחזירה מערך של מחרוזות לתיקיות וקבצים בתוך התיקייה ששלחנו לה. במקרה הזה, נקבל בחזרה מערך של מחרוזות של קבצים ותיקיות בתוך תיקיית המקור. נעבור בלולאה על הערכים שהוחזרו:
foreach (string Element in Files)
נבדוק אם המחרוזת הנוכחית היא תיקייה ואם כן נקרא למתודה שלנו באופן רקורסיבי על התיקייה הזו:
// Sub directories
if (Directory.Exists(Element))
copyDirectory(Element, Dst + Path.GetFileName(Element));
נבדוק אם המחרוזת הנוכחית היא קובץ ואם כן נדאג להעתיק אותו מהמקור ליעד:
// Files in directory
else
File.Copy(Element, Dst + Path.GetFileName(Element), true);
וזהו. סה"כ המתודה הזו דואגת להעתיק ספרייה ממקום אחד למקום השני. בואו נראה כיצד נוכל לעבוד עימה:
private void btnCopyCmyDirToCmyOtherDir_Click(object sender, EventArgs e)
{
if (Directory.Exists(@"C:\myDir"))
{
copyDirectory(@"C:\myDir", @"c:\myOtherDir");
AddNewRow(@"Copied C:\myDir to C:\myOtherDir");
}
}
העתקנו את c:\myDir לתוך C:\myOtherDir.
חלק ה': FileSystemWatcher (או: "לתפוס את כל השינויים ברגע שהם קורים")
נכיר מחלקה חדשה FileSystemWatcher. כשמה כן היא - צופה על קבצים ותיקיות. המחלקה הזו היא מן "מרגל" שאנו יכולים לשים על מגוון של אירועים וסוגי התראות על מיקום כלשהו בכונן כלשהו שיידע מה לעשות כאשר אותם אירועים קופצים. למשל בוא נראה דוגמה מאוד פשוטה שבה נשתמש ב-FileSystemWatcher על C:\myDir ונגיד לה להדפיס כל פעם שמוסיפים קובץ חדש.
private void btnWatchForNewFilesOnCmyDir_Click(object sender, EventArgs e)
{
FileSystemWatcher watcher = new FileSystemWatcher(@"C:\myDir");
watcher.NotifyFilter = NotifyFilters.FileName;
watcher.Created += new FileSystemEventHandler(watcher_Created);
watcher.EnableRaisingEvents = true;
}
void watcher_Created(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been created!");
}
נעבור שורה-שורה ונראה מה בדיוק הולך כאן.
FileSystemWatcher watcher = new FileSystemWatcher(@"C:\myDir");
יצרנו מופע חדש של FileSystemWatcher על התיקייה C:\myDir. לא התחלנו שום תהליך, פשוט אתחלנו את המחלקה החדשה.
watcher.NotifyFilter = NotifyFilters.FileName;
נגיד ל-FileSystemWatcher שלנו שיחפש שינויים בשם הקובץ. המאפיין FileSystemWatcher.NotifyFilter הוא enum שמייצג לאיזה סוגי שינויים בדיוק ה-watcher צריך להקשיב. הרי אם אנחנו מצפים לתפוס הוספת קבצים חדשים השנוי יתפס כי יהיה "שם קובץ" חדש.
watcher.Created += new FileSystemEventHandler(watcher_Created);
FileSystemWatcher חושף בפנינו מגוון של אירועים שאליהם אנו יכולים "להירשם". כל מודל האירועים של דוט נט אומר ככה "אנו אחשוף לך אירוע, אתה תרשום לי איזה מתודה אתה רוצה שאני אזמן שהאירוע עולה". במקרה הזה רשמנו את המתודה watcher_Created לאירוע Created. כלומר, כל פעם שה-watcher שלנו ישים לב שיש קובץ חדש - המתודה watcher_Created תופעל. בואו נביט על המתודה:
void watcher_Created(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been created!");
}
החתימה של כל מתודה כזו שאנו "רושמים" לאירוע מוגדרת כחלק מהאירוע. כל מתודה שנרשמת ל-Created חייבת להחזיר void, ולקבל שני פרמטרים: הראשון sender מסוג object, והשני e מסוג FileSystemEventArgs. נהוג שכל מתודה ש"רושמים" לאירוע תמיד תקבל שני פרמרטרים - sender מסוג אובייקט, ו-System.EventArgs (או מחלקה היורשת ממנה) שתכיל את כל המידע הרלוונטי לאירוע.
במקרה שלנו sender הוא ה-FileSystemWatcher (וגם נוכל להמיר אותו באופן מפורש לסוג המחלקה הזה), וה-e הוא FileSystemEventArgs. המחלקה הזו FileSystemEventArgs מכילה את כל המידע הרלוונטי למדוע הוקפץ האירוע הזה. במקרה הזה, נוכל לגשת ל-e.FullName כדי לקבל את שם הקובץ שגרם לאירוע לקפוץ. נעשה את זה ונקפיץ למשתמש הודעה "היי, תראה, הקובץ הזה נוצר!".
watcher.EnableRaisingEvents = true;
השורה הזו במתודה הראשית שלנו אומרת ל-watcher "או קיי, סיימנו להכין את ה-watcher עכשיו אתה תכלס' יכול להתחיל לצפות".
בואו נראה את התוצאות: (נשתמש בכל הכפתורים שיצרנו קודם לכן כדי ליצור את הקבצים)
מחקנו את c:\myDir\myFile.txt, התחלנו את ה-FileSystemWatcher שלנו, ויצרנו את הקובץ c:\myDir\myFile.txt. וישר אחר כך קיבלנו הודעה שבאמת c:\myDir\myFile.txt נוצר.
בואו נביט על הערכים האפשריים ל-FileSystemWatcher.NotifyFilter:
-
Attributes - שינויים ב-Attributes של הקובץ\תיקייה.
-
CreationTime - שינויים בזמן יצירת הקובץ\תיקייה.
-
DirectoryName - שינויים בשם תיקייה.
-
FileName - שינויים בשם קובץ.
-
LastAccess - שינויים כאשר פותחים את הקובץ\תיקייה.
-
LastWrite - שינויים כאשר עורכים את הקובץ\תיקייה.
-
Security - שינויים באבטחה של Windows על הקובץ\תיקייה.
-
Size - שינויים בגודל הקובץ\תיקייה.
אלו הם ה"סממנים" שה-FileSystemWatcher שלנו מקשיב להם מתחת לפני השטח. הם רק אומרים לו איזה סימנים לחפש לשינויים שיכולים לקרות.
לאילו אירועים אנחנו יכולים להירשם ב-FileSystemWatcher?
-
Created - קופץ כאשר קובץ\תיקייה חדשה נוצרת.
-
Deleted - קופץ כאשר קובץ\תיקייה נמחקת.
-
Changed - קופץ כאשר יש שינויים בקובץ\תקייה כפי שהוגדר ב-NotifyFilter של ה-FileSystemWatcher.
-
Renamed - קופץ כאשר משנים את שם תיקייה\קובץ.
-
Error - קופץ בשגיאה מערכת פנימית. נדבר על זה עוד בהמשך.
בואו עכשיו נתחיל ביצירת FileSystemWatcher האולטימטיבי שיממש את כל מה שלמדנו ונלמד. אז ניצור FileSystemWatcher שצופה על כל שינוי אפשרי מבחינת סממנים של שינוי (NotifyFilter) ויגיב לאירועים Created, Deleted, ו-Changed.
private void button1_Click(object sender, EventArgs e)
{
FileSystemWatcher watcher = new FileSystemWatcher(@"C:\myDir");
watcher.NotifyFilter = NotifyFilters.Attributes
| NotifyFilters.CreationTime
| NotifyFilters.DirectoryName
| NotifyFilters.FileName
| NotifyFilters.LastAccess
| NotifyFilters.LastWrite
| NotifyFilters.Security
| NotifyFilters.Size ;
watcher.Created += new FileSystemEventHandler(watcher_Created);
watcher.Changed += new FileSystemEventHandler(watcher_Changed);
watcher.Deleted += new FileSystemEventHandler(watcher_Deleted);
watcher.EnableRaisingEvents = true;
AddNewRow(@"FileSystemWatcher for created, deleted and changed files on C:\myDir started...");
}
void watcher_Created(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been created!");
}
void watcher_Deleted(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been deleted!");
}
void watcher_Changed(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been changed!");
}
בוא נעבור על מה שעשינו. שמנו NotifyFilter לכל הערכים שאנו מכירים (שמתם לב אגב ש-NotifyFilter הוא Enum flags כמו Attributes שראינו קודם?). רשמנו מתודה לכל אירוע רלוונטי - Created, Deleted ו-Changed. בואו נראה את התוצאה של איך מגיב ה-watcher שלנו למחיקת קובץ:
ובואו נראה איך הוא מגיב להוספת "Hello world" לסוף הקובץ:
בואו נוסיף עכשיו מתודה שתגיב לאירוע Renamed.
private void btnSetFileWatcherOnCmyDirWithAllFileEventsAndRenamed_Click(object sender, EventArgs e)
{
FileSystemWatcher watcher = new FileSystemWatcher(@"C:\myDir");
watcher.NotifyFilter = NotifyFilters.Attributes
| NotifyFilters.CreationTime
| NotifyFilters.DirectoryName
| NotifyFilters.FileName
| NotifyFilters.LastAccess
| NotifyFilters.LastWrite
| NotifyFilters.Security
| NotifyFilters.Size;
watcher.Created += new FileSystemEventHandler(watcher_Created);
watcher.Changed += new FileSystemEventHandler(watcher_Changed);
watcher.Deleted += new FileSystemEventHandler(watcher_Deleted);
watcher.Renamed += new RenamedEventHandler(watcher_Renamed);
watcher.EnableRaisingEvents = true;
AddNewRow(@"FileSystemWatcher for created, deleted, changed & renamed files on C:\myDir started...");
}
void watcher_Renamed(object sender, RenamedEventArgs e)
{
MessageBox.Show(e.OldFullPath + " Has been renamed to " + e.FullPath);
}
void watcher_Deleted(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been deleted!");
}
void watcher_Changed(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been changed!");
}
void watcher_Created(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been created!");
}
שימו לב לשינוי בין RenamedEventArgs לבין FileSystemEventArgs. כאשר אנו משנים את השם של הקובץ ה-RenamedEventArgs מכילים גם את השם הישן של הקובץ וגם את השם החדש של הקובץ. בואו נראה את התוצאות של הפעלת ה-watcher ושינוי שם לקובץ שלנו:
עכשיו נרצה גם להוסיף צפייה על התת-תיקיות של c:\myDir לכל השינויים שאנו צופים בהם עד כה. כלומר, לתפוס יצירת קובץ חדש, מחיקה, שינוי שם או שינוי אחר בכל אחת מהתת-תיקיות של C:\myDir. בשביל זה קיים המאפיין הבוליאני FileSystemWatcher.IncludeSubdirectories שקובע האם ה-watcher צופה גם על התת-תיקיות.
private void btnAllFileEventAdnRenamedInCmyDirAndSubdirs_Click(object sender, EventArgs e)
{
FileSystemWatcher watcher = new FileSystemWatcher(@"C:\myDir");
watcher.NotifyFilter = NotifyFilters.Attributes
| NotifyFilters.CreationTime
| NotifyFilters.DirectoryName
| NotifyFilters.FileName
| NotifyFilters.LastAccess
| NotifyFilters.LastWrite
| NotifyFilters.Security
| NotifyFilters.Size;
watcher.Created += new FileSystemEventHandler(watcher_Created);
watcher.Changed += new FileSystemEventHandler(watcher_Changed);
watcher.Deleted += new FileSystemEventHandler(watcher_Deleted);
watcher.Renamed += new RenamedEventHandler(watcher_Renamed);
watcher.IncludeSubdirectories = true;
watcher.EnableRaisingEvents = true;
AddNewRow(@"FileSystemWatcher for created, deleted, changed & renamed files on C:\myDir started...");
}
נראה כיצד מגיב ה-watcher שלנו לכך שאנו מעבירים את c:\mydir\myFile.txt לתוך c:\myDir\myOtherDir:
אפשר לראות שקלטנו ארבעה אירועים:
-
מחיקת הקובץ c:\myDir\myFile.txt
-
יצירת הקובץ c:\myDir\SomeOtherDir\myFile.txt
-
שינוי בתיקייה C:\myDir\myOtherDir
-
שינוי בקובץ C:\myDir\SomeOtherDir\myFile.txt
שימו לב שאת האירוע השני והרביעי קלטנו בתוך התקייה c:\myDir\SomeOtherDir רק מפאת שציינו שאנו רוצים שה-watcher יקשיב גם לתת-תיקיות.
בואו נדבר על האירוע המסתורי FileSystemWatcher.Error. חשבתם שכל האירועים האלו שנתפסים זה חינם? תחשבו מה יכול לקרות אם היינו שמים FileSystemWatcher על תיקייה שיש בה אלפי שינויים כל שנייה - אנו נצטרך לתפוס, לנתח ולהגיב לאלפי אירועים כל שנייה. מאחורי הקלעים מערכת ההפעלה מעבירה באמצעות Buffer (מקום אחסון זמני וקטן) לתוכנה שלנו מה כל השינויים. למעשה, מערכת ההפעלה "רושמת" ב-Buffer הזה את כל השינויים שאנחנו ביקשנו לתפוס והפריימוורק באה לשם ומנקה את ה-Buffer. אבל אם יש אלפי אירועים, ה-Buffer יכול להגיע למקסימום שלו. יכול להיות שמערכת ההפעלה קלטה העברה של 1,000 קבצים ממקום למקום שני - וזה ירצח את ה-Buffer שלנו.
פתרון ראשון למצב כזה זה לעלות את גודל ה-FileSystemWatcher.InternalBufferSize שזה מייצג את הגודל בביטים של ה-Buffer שלנו. ברירת המחדל היא שמונה קילו בייט. מומלץ בחום לא לשנות את גודל ה-Buffer אלא כמפלט אחרון. ה-Buffer הזה יושב על זכרון ה-RAM וכל השרת שלנו יכול ליפול אם נפתח כמה watcherים שכל אחד מהם תופס יותר מדי מקום ב-RAM.
פתרון שני למצב הזה, הוא למקד את על מה ה-FileSystemWatcher שלנו צריך לצפות. אם צריך לצפות רק על תיקייה ספציפית, לשים את ה-watcher רק אליה. אם צריך לצפות רק בקובץ מסויים, לשים את ה-watcher רק אליו. אם לא צריך לצפות בתת-תיקיות, לא לאפשר לצפות בתת-תיקיות. אם צריך לצפות בסוג מסויים בלבד של קבצים לדאוג שה-watcher צופה רק בסוג אחד של קבצים.
אבל בכל זאת יכול להיות שנגיע למצב שבו ה-Buffer הגיע למקסימום שלו. במקרה כזה עולה האירוע Error. סביר להניח שנרצה לרשום את זה לאיזה לוג, לשלוח דוא"ל או להדליק אור אדום מהבהב ואזעקה. נרשום מתודה לאירוע הזה ונראה איך נראה הקוד הסופי שלנו:
private void btnSetFileWatcherOnCmyDirWithAllFileEventsAndRenamedAndError_Click(object sender, EventArgs e)
{
FileSystemWatcher watcher = new FileSystemWatcher(@"C:\myDir");
watcher.NotifyFilter = NotifyFilters.Attributes
| NotifyFilters.CreationTime
| NotifyFilters.DirectoryName
| NotifyFilters.FileName
| NotifyFilters.LastAccess
| NotifyFilters.LastWrite
| NotifyFilters.Security
| NotifyFilters.Size;
watcher.Created += new FileSystemEventHandler(watcher_Created);
watcher.Changed += new FileSystemEventHandler(watcher_Changed);
watcher.Deleted += new FileSystemEventHandler(watcher_Deleted);
watcher.Renamed += new RenamedEventHandler(watcher_Renamed);
watcher.Error += new ErrorEventHandler(watcher_Error);
watcher.IncludeSubdirectories = true;
watcher.EnableRaisingEvents = true;
AddNewRow(@"FileSystemWatcher for created, deleted, changed & renamed files on C:\myDir and subdirs started...");
}
void watcher_Error(object sender, ErrorEventArgs e)
{
// log this error
// send an email
// send a pager message
// flash alert warning and make sounds like it's armageddon!
}
void watcher_Renamed(object sender, RenamedEventArgs e)
{
MessageBox.Show(e.OldFullPath + " Has been renamed to " + e.FullPath);
}
void watcher_Deleted(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been deleted!");
}
void watcher_Changed(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been changed!");
}
void watcher_Created(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.FullPath + " Has been created!");
}
סיכום
למי שרוצה להוריד את הדוגמאות קוד והפרוייקט שעבדנו עליו אפשר להוריד מכאן:
http://www.JustinAngel.net/files/WinSystemIO.Zip
בואו נראה את הרשימת נושאים של מיקרוסופט ל-System.IO ונראה שכיסינו את הכל:
Access files and folders by using the File System classes.
- File class and FileInfo class - המחלקה File היא המחלקה עם המתודות הסטטיות לעבודה עם קבצים, והמחלקה FileInfo היא המחלקה הקונקרטית לעבודה עם קבצים.
- Directory class and DirectoryInfo class - המחלקה Directory היא המחלקה עם המתודות הסטטיות לעבודה עם תיקיות, והמחלקה DirectoryInfo היא המחלקה הקונקרטית לעבודה עם תיקיות.
- DriveInfo class and DriveType enumeration - המחלקה DriveInfo היא מחלקה קונקרטית שמכילה את האינפורמציה והמתודות לכל כונן, וכמו כן מתודה סטטית GetDrives שמאפשרת להחזיר את כל הכוננים. ה-DriveType enum הוא ה-enum שאומר איזה סוג כונן הוא הכונן הנוכחי.
- FileSystemInfo class - לא ראינו את המחלקה הזאת ואין צורך. היא פשוט מחלקה קונקרטית שיכולה להתאים גם לקבצים וגם לתיקיות. יוצרים אותה כמו שיוצרים DirectoryInfo או FileInfo ועובדים איתה בדיוק כמו שעובדים DirectoryInfo ו-FileInfo.
- FileSystemWatcher class - המחלקה שמאפשרת לקבל התראה כאשר יש שינויים במערכת הקבצים.
- Path class - ראינו אותה לרגע. בקצרה, היא מן מחלקת עזר שמכילה כל מיני פונקציות בנאליות כמו "חיבור שתי נתיבים לנתיב אחד" (שזה כמעט שקול אחד לאחד לחיבור של שתי מחרוזות). זאת מחלקת עזר נחמדה ואפשר להסתדר בלעדיה.
- ErrorEventArgs class and ErrorEventHandler delegate - זה ה-delegate שמצביע למתודה שקופצת כאשר יש חריגה מה-Buffer וה-EventArgs האלו. למי שתוהה, ה-ErrorEventArgs אינם מכילים שום מידע.
- RenamedEventArgs class and RenamedEventHandler delegate - זה ה-delegate שמצביע למתודה שקופצת כאשר משנים את השם של קובץ\תיקייה. ה-RenamedEventArgs מכילים את השם החדש והישן של הקובץ\תיקייה.
ברכות,
ג'סטין-יוסף אנג'ל