Requirements
The tool needs to be able to search a directory tree for files with a certain extensions (.xaml*) for a pattern or literal string. Before it does this it also needs to be able to open a .xaml file and retrieve any style elements so it can then read their keys.
To achieve this, two classes are needed. One class will read a .xaml file and get all keys from style elements and the other class will search trough the file system for files containing these keys.
Building it
I'll spare the obvious details and dive right into the highlights. To read the keys from style elements in basically any xml document, I used LinqToXml. Here is the code I used:
private void LoadStyleKeysFromDocument()
{
XNamespace winFxNamespace = "http://schemas.microsoft.com/winfx/2006/xaml";
XName keyAttributeName = winFxNamespace + "Key";
var result = from node in _document.Descendants()
where node.Name.LocalName.Equals("Style")
select node;
var distinctResult = result.Distinct();
StyleKeys.Clear();
foreach (XElement styleElement in distinctResult)
{
StyleKeys.Add(styleElement.Attributes(keyAttributeName).First().Value);
}
}
{
XNamespace winFxNamespace = "http://schemas.microsoft.com/winfx/2006/xaml";
XName keyAttributeName = winFxNamespace + "Key";
var result = from node in _document.Descendants()
where node.Name.LocalName.Equals("Style")
select node;
var distinctResult = result.Distinct();
StyleKeys.Clear();
foreach (XElement styleElement in distinctResult)
{
StyleKeys.Add(styleElement.Attributes(keyAttributeName).First().Value);
}
}
The first two lines make an XName object that is needed to include the xml namespace when retrieving the x:Key from the element. Note that this works independently from the prefix (x) as it was assigned in the document. This means that this code will still work if someone would decide to change the prefix on this namespace.
Next, a Linq query is used to retrieve any nodes in the document that have the name Style. The query is followed by a statement to make sure I only get unique results.
Finally I fill the StyleKeys collection with any key attributes value found inside an element in the query result.
Searching for a particular pattern in the file system is done in the following method:
public void Search(string pattern, string rootFolder, string fileFilter)
{
// Get all files matching the filter
string[] fileNames = Directory.GetFiles(rootFolder, fileFilter, SearchOption.AllDirectories);
// For each file
foreach (string fileName in fileNames)
{
// Open file
string fileData = File.ReadAllText(fileName);
// Match pattern
MatchCollection matches = Regex.Matches(fileData, pattern);
// Register count
PatternSearchResultEntry resultEntry = newPatternSearchResultEntry()
{
FileName = fileName,
HitCount = matches.Count,
Pattern = pattern
};
Results.Add(resultEntry);
}
}
{
// Get all files matching the filter
string[] fileNames = Directory.GetFiles(rootFolder, fileFilter, SearchOption.AllDirectories);
// For each file
foreach (string fileName in fileNames)
{
// Open file
string fileData = File.ReadAllText(fileName);
// Match pattern
MatchCollection matches = Regex.Matches(fileData, pattern);
// Register count
PatternSearchResultEntry resultEntry = newPatternSearchResultEntry()
{
FileName = fileName,
HitCount = matches.Count,
Pattern = pattern
};
Results.Add(resultEntry);
}
}
As you can see, the first line gets all the filenames that are anywhere in the directory hierarchy below the supplied root folder.
Looping through the filenames, I simply load all the text from each file and use the Regex class to count the number of hits. By doing so, this code is also very useful to find hit counts for other patterns.
All the results are added to a collection of a struct called PatternSearchResultEntry.
So thats the business end of things. Obviously we need a user interface of some sort.
I chose a WPF interface, because I like data binding.
To retrieve user input for the style file and the folder to look in, I build a class called BindableString, which contains a Name and a Value and implements the INotifyPropertyChanged interface. It allows me to create instances of these and bind them to my UI. This way I have a central point to access this information without having to worry about updates, etc..
To do the actual work I wrote the following Click event for a button:
private void analyseStyleUsageButton_Click(object sender, RoutedEventArgs e)
{
XamlStyleKeyReader reader = newXamlStyleKeyReader();
reader.ReadXamlFile(_stylesFilePath.Value);
PatternSearch patternSearch = newPatternSearch();
foreach (string styleKey in reader.StyleKeys)
{
patternSearch.Search(styleKey, _searchRootDirectory.Value, new string[] { "*.xaml" });
}
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(
patternSearch.Results);
if (view.CanGroup)
{
view.GroupDescriptions.Add(new PropertyGroupDescription("Pattern"));
}
analyseStyleUsageDataGrid.ItemsSource = view.Groups;
}
{
XamlStyleKeyReader reader = newXamlStyleKeyReader();
reader.ReadXamlFile(_stylesFilePath.Value);
PatternSearch patternSearch = newPatternSearch();
foreach (string styleKey in reader.StyleKeys)
{
patternSearch.Search(styleKey, _searchRootDirectory.Value, new string[] { "*.xaml" });
}
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(
patternSearch.Results);
if (view.CanGroup)
{
view.GroupDescriptions.Add(new PropertyGroupDescription("Pattern"));
}
analyseStyleUsageDataGrid.ItemsSource = view.Groups;
}
It basically instantiates the XamlStyleKeyReader class and loads the style file in it. Next it instantiates the PatternSearch class and kicks of a search for each style key available in the XamlStyleKeyReader.
The code after that groups the results based on the search pattern. The reason I did it this way is because it is not very transparent to bind to the result of a group in Linq. Binding to this is easy once you know how. As you can see the items source for the datagrid that displays my results, is actually the collection of groups.
This collection is declared as having objects, which isn't very helpful, however diving into the API documentation reviels that this collection contains instances of the CollectionViewGroup class. From that class I need the name (obviously) and a hit count, which of course it doesn't have.
To get a hit count I bound to the Items property from the group, which contains all the items that belong to that group and then I use a value converter to get the total hit count for that group.
I've uploaded the complete source for this tool here.
Be aware that this tool is far from finished. I would like to save the last settings and have some progress indication, which means moving the search code to it's own thread. Styling of the UI can be improved, etc., etc.
I do hope you find this code useful and you've learned something along the way.
No comments:
Post a Comment