Yesterday I needed to check the available style keys in our main app.xaml file and see which ones are no longer needed. As there are currently 66 style keys in that file and it's growing, I didn't feel much for taking each key and searching through our source code to find out. Time to build a small tool. This article describes how I build this tool.
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 through 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);
}
}
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)
{
string[] fileNames = Directory.GetFiles
(rootFolder, fileFilter, SearchOption.AllDirectories);
foreach (string fileName in fileNames)
{
string fileData = File.ReadAllText(fileName);
MatchCollection matches = Regex.Matches(fileData, pattern);
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 that's 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;
}
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 reveals 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 its own thread. Styling of the UI can be improved, etc.
I do hope you find this code useful and you've learned something along the way.