- Create a new Console Application project in Visual Studio.
- Add to the project a new class named FileInfoViewer which will store
information about files located in the specified directory and display
some information on the console.
- Add to the class a List<FileInfo> member to store
information about files. Fill this list in the constructor using
a path passed as a parameter:
List<FileInfo>
filesInfo;
public
FileInfoViewer(string
path)
{
string[]
filesPaths = Directory.GetFiles(path);
if
(filesPaths.Length > 0)
{
filesInfo =
new
List<FileInfo>(filesPaths.Length);
foreach
(string
filePath in
filesPaths)
{
FileInfo
fileInfo = new
FileInfo(filePath);
filesInfo.Add(fileInfo);
}
}
else
{
filesInfo =
new
List<FileInfo>();
}
}
- Create a method formatting information of the FileInfo
object. It can be an extension method, located in a new class
named e.g. FileInfoExtensions:
static
class
FileInfoExtensions
{
public
static
string
Format(this
FileInfo
fi)
{
return
string.Format("[
{0,-38} | {1,12:0,0} | {2} ]",
fi.Name, fi.Length, fi.CreationTime);
}
}
Special formatting options were chosen to assure proper look
of strings in the console.
- Add to the FileInfoViewer class a method displaying
information about files processed in the constructor. Using the
new Format method and a lambda expression, one line of code is
enough:
public
void
ListFiles()
{
filesInfo.ForEach(fi => {
Console.WriteLine(fi.Format());
});
}
- Test if everything works, e.g. using the following code in
the Program.Main method:
FileInfoViewer
viewer = new
FileInfoViewer(@"C:\Windows");
viewer.ListFiles();
- To make the work of adding and testing new methods of the
FileInfoViewer class easier, write code which will look for 'special'
methods and build a menu for the user to allow to execute these methods.
- To mark a method as 'special', custom attribute will be
used. Create a new class named RunnableMethodAttribute inherited
from the Attribute class. Leave this class empty:
class
RunnableMethodAttribute
: Attribute
{
}
- Apply this new attribute to the FileInfoViewer.ListFiles
method:
[RunnableMethod]
public
void
ListFiles()
{
filesInfo.ForEach(fi => {
Console.WriteLine(fi.Format());
});
}
- Add to the Program class code creating and displaying a menu
for the user:
class
Program
{
static
Dictionary<char,
MethodInfo>
menu;
static
FileInfoViewer
viewer;
static
void
BuildMenu()
{
menu =
new
Dictionary<char,
MethodInfo>();
char
c = 'A';
var
methods = (IEnumerable<MethodInfo>)viewer.GetType().GetMethods().Reverse();
foreach
(MethodInfo
mi in
methods)
{
if
(mi.GetCustomAttributes(typeof(RunnableMethodAttribute),
false).Length
> 0)
{
menu.Add(c, mi);
c++;
}
}
}
static
void
ShowMenu()
{
foreach
(KeyValuePair<char,
MethodInfo>
keyValue in
menu)
{
Console.WriteLine("[{0}]
{1}", keyValue.Key,
keyValue.Value.Name);
}
}
static
bool
ProcessMenu()
{
Console.WriteLine("--------------------------------");
ShowMenu();
Console.WriteLine("[0]
-- exit");
string
s;
char
c;
do
{
s =
Console.ReadLine();
c = (s.Length > 0) ?
char.ToUpper(s[0])
: ' ';
} while
(c != '0'
&& !menu.ContainsKey(c));
if
(c == '0')
{
return
false;
}
else
{
MethodInfo
mi = menu[c];
mi.Invoke(viewer,
new
object[]
{});
return
true;
}
}
static
void
Main(string[]
args)
{
viewer =
new
FileInfoViewer(@"C:\Windows");
BuildMenu();
while
(ProcessMenu())
{
}
}
}
Note the first using of the LINQ in this tutorial - the
Reverse method of the IEnumerable<T> class.
(Reversing the
order of listed methods is done only for our convenience - the
last method will appear on the first place in the menu.)
- Run the application and test the menu:
--------------------------------
[A]
ListFiles
[0] -- exit
- Add to the FileInfoViewer class a private method that will be able
to write to the console a list of FileInfo objects. To allow to use the
method for any type of collection (e.g. an array or a generic list),
take the collection to list as an IEnumerable<FileInfo> object:
private
void
ListContent(IEnumerable<FileInfo>
toList)
{
foreach
(FileInfo
fi in
toList)
{
Console.WriteLine(fi.Format());
}
}
- Write a public method of the FileInfoViewer class listing
files bigger then 1 MB. Remember to apply the RunnableMethodAttribute to the method, to
display it
automatically in the menu.
[RunnableMethod]
public
void
BiggerThan1MB()
{
long
megabyte = 1024 * 1024;
var
chosen =
from
fi in
filesInfo
where
fi.Length > megabyte
select
fi;
ListContent(chosen);
}
It is a quite simple query with a single condition.
- Now, list all files containing in the name at least one letter from
their extensions:
[RunnableMethod]
public
void
ContainingLetterFromExtension()
{
var
chosen =
from
fi in
filesInfo
where
fi.GetNameWithoutExtension().IndexOfAny(fi.Extension.ToCharArray())
>= 0
select
fi;
ListContent(chosen);
}
Where the GetNameWithoutExtension method is an extension method
(note the this keyword before the first parameter) of
the FileInfoExtensions class:
public
static
string
GetNameWithoutExtension(this
FileInfo
fi)
{
return
fi.Name.Substring(0, fi.Name.Length - fi.Extension.Length);
}
- List 10 newest files, sorted by the last access time.
There
is no special operator for the Take method, so the normal syntax of
calling a method must be used.
[RunnableMethod]
public
void
Top10Newest()
{
var
chosen =
(from
fi in
filesInfo
orderby
fi.LastAccessTime descending
select fi)
.Take(10);
ListContent(chosen);
}
Default sorting is ascending, to change the direction, the 'descending'
operator must be used.
- To show the smallest file with the last access time from the current
year, the following code can be used:
[RunnableMethod]
public
void
SmallestFromCurrentYear()
{
var
chosen =
(from
fi in
filesInfo
where
fi.LastAccessTime.Year ==
DateTime.Now.Year + 10
orderby
fi.Length
select
fi)
.FirstOrDefault();
Console.WriteLine(chosen
== null
? "no such file"
: chosen.Name);
}
Note that in case of no files with the last access time from the
current year, using First instead FirstOrDefault would throw an
exception.
- The next task is to answer a question of how many files were last
accessed in the current year and in previous years.
[RunnableMethod]
public
void
NumberFilesPerYear()
{
var
chosen =
from
fi in
filesInfo
group
fi by
fi.LastAccessTime.Year into
byYears
orderby
byYears.Key
select
new
{ Year = byYears.Key, Count = byYears.Count() };
foreach
(var
ch in
chosen)
{
Console.WriteLine("{0}:
{1} files", ch.Year, ch.Count);
}
}
The special 'group' operator can be used for grouping selected
objects. The object named after the 'into' operator is a collection
storing data of one group of objects. It has the special 'Key' property
with value of the key of grouping.
- The next task is similar to the previous one - group files (this
time using their extensions) and show some statistics of groups: number
of files, minimum size, and maximum size.
[RunnableMethod]
public
void
MaxMinSizePerExtension()
{
var
chosen =
from
fi in
filesInfo
group
fi by
fi.Extension.ToUpper() into
g
orderby
g.Key
select
new
{
Extension = g.Key,
Count = g.Count(),
MaxSize = g.Max(a => a.Length),
MinSize =
g.Min(a => a.Length)
};
foreach
(var
ch in
chosen)
{
Console.WriteLine("{0,-10}:
{1,2} files, min size: {2,8}, max size: {3,8}",
ch.Extension, ch.Count, ch.MinSize, ch.MaxSize);
}
}
The same expression can be also written using nested select operators:
[RunnableMethod]
public
void
MaxMinSizePerExtension_Alt()
{
var
chosen =
from
fi in
filesInfo
group
fi by
fi.Extension.ToUpper() into
g
orderby
g.Key
select
new
{
Extension = g.Key,
Count = g.Count(),
MaxSize = (from
a in
g select
a.Length).Max(),
MinSize = (from
a in
g select
a.Length).Min(),
};
foreach
(var
ch in
chosen)
{
Console.WriteLine("{0,-10}:
{1,2} files, min size: {2,8}, max size: {3,8}",
ch.Extension, ch.Count, ch.MinSize, ch.MaxSize);
}
}
- Of course, not only aggregation functions are available for groups,
items contained by the grouped collections can also be available. Write
code grouping files by their extensions and listing grouped files:
[RunnableMethod]
public
void
FilesGroupedByExtension()
{
var
chosen =
from
fi in
filesInfo
group
fi by
fi.Extension.ToUpper() into
g
orderby
g.Key
select
new
{ Extension = g.Key, FilesInfo = g };
foreach
(var
ch in
chosen)
{
Console.WriteLine();
Console.WriteLine("{0}:
", ch.Extension);
ListContent(ch.FilesInfo);
}
}
- If we want to see only a list of extensions, there is no need for
grouping:
[RunnableMethod]
public
void
ListExtensions()
{
var
chosen =
(from
fi in
filesInfo
select
fi.Extension.ToUpper())
.Distinct();
foreach
(var
ch in
chosen)
{
Console.WriteLine(ch);
}
}
- A rather simple task to answer a quesion of total size of all files
can be solved using new LINQ operators:
[RunnableMethod]
public
void
TotalSize()
{
long
totalSize =
(from
fi in
filesInfo
select
fi.Length)
.Sum();
Console.WriteLine("Total
size: {0:0,0}", totalSize);
}
or simpler:
[RunnableMethod]
public
void
TotalSize_Alt()
{
long
totalSize = filesInfo.Sum(fi => fi.Length);
Console.WriteLine("Total
size: {0:0,0}", totalSize);
}
- More complicated example: list only files containing in their names
a
name of any other file.
[RunnableMethod]
public
void
ContainingNames()
{
var
chosen =
from
lookedFor in
filesInfo
from
searched in
filesInfo
where
searched.Name.Contains(lookedFor.Name.Substring(0,
lookedFor.Name.Length - lookedFor.Extension.Length)) &&
searched.Name != lookedFor.Name
select
new
{ SubName = lookedFor.Name, Name = searched.Name };
foreach
(var
ch in
chosen)
{
Console.WriteLine("{0}:
{1}", ch.SubName, ch.Name);
}
}
In this example the 'where' operator uses an expression using 2
iterated variables. This expression could be simpler in case of using a
method returning a name without an extension for a FileInfo object (the
GetNameWithoutExtension method from p.6). Using this method, the expression
is more readable:
[RunnableMethod]
public
void
ContainingNames_Alt()
{
var
chosen =
from
lookedFor in
filesInfo
from
searched in
filesInfo
where
searched.Name.Contains(lookedFor.GetNameWithoutExtension()) &&
searched.Name != lookedFor.Name
select
new
{ SubName = lookedFor.Name, Name = searched.Name };
foreach
(var
ch in
chosen)
{
Console.WriteLine("{0}:
{1}", ch.SubName, ch.Name);
}
}