This blogpost that I’m writing is a prequel to another blogpost about best practices in navigation in MVVM.
I came upon the idea the other day by extracting the URI of a page by its location in the assembly. This seemed easier than I thought and if anyone knows of a simpeler solution then I implore you to post it.
I searched for any quick fixes like some magic property on the page that would tell me where the XAML is stored inside the assembly, but no luck. I even decompiled a few pages to see what’s in the InitialiseComponent and I was deeply surprised that MS has opted for such a weak implementation. This is what I found:
Application.LoadComponent(this, new Uri("/MyAssembly;component/Views/MyPage.xaml", UriKind.Relative));
So by judging at how the XAML page is loaded, there is no property or wrapper class that will tell me where the page is stored.
Then while I was browsing the assembly, I noticed that in the assembly.g.resources file is a listing of all the resources in the assembly (hence the name…). I saw that not only embedded resources are there but also the XAML of your pages. It is structured by a dictionary that contains a key/value pair of string / memorystream. So I thought, this should be easy then, I can just read the resourcemanifest and deserialize it … Microsoft wouldn’t be Microsoft if they didn’t make all the classes you need to do this internal. So I had to manually get out the pages from the binary data. Enough gibberish, here is the code:
public class PageUriLocator
{
public static string GetUri<T>() where T : Page
{
var typeName = typeof(T).Name;
var allXamlFiles = ReadAssemblyResource();
var fileName = new Regex(@"[[\w]*/]?" + typeName.ToLower() + @"\.xaml");
return (allXamlFiles.Where(xamlFile => fileName.IsMatch(xamlFile.ToLower()))
.Select(xamlFile => xamlFile))
.FirstOrDefault();
}
private static List<string> ReadAssemblyResource()
{
byte[] binaryData = GetResourceInByteArray();
var fileNames = new List<string>();
// the first x bytes of the resource stream we are not interested in
bool start = false;
for (int i = 0; i < binaryData.Length; i++)
{
if (i < binaryData.Length - 255 && i > 0)
{
// the first file in the resource stream is app.xaml => it is 00 00 10 in size
start = start | (binaryData[i] == 0x00 && binaryData[i + 1] == 0x00 && binaryData[i + 2] == 0x10);
if (start)
{
int length = binaryData[i + 1] + binaryData[i + 2]; // the length is described in the two latter bytes of the 5 stopbytes
if (length > 255) return fileNames; // name cannot be longer then 255 characters
// assemble the filename
string fileName = "";
for (int j = 0; j < length; j += 2)
{
fileName += Convert.ToChar(binaryData[j + i + 3]);
}
fileNames.Add(fileName);
i += length + 4;
}
}
}
return fileNames;
}
private static byte[] GetResourceInByteArray()
{
// get the assembly name
var assemblyName = Assembly.GetExecutingAssembly().FullName.Split(',')[0];
// a list of resources is saved in an embedded resource file called [AssemblyName].g.resources
var resourceName = assemblyName + ".g.resources";
// read binary data from the stream
var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
var streamReader = new BinaryReader(resourceStream);
return streamReader.ReadBytes((int)resourceStream.Length);
}
}
And the usage:
string whereIsThatDamnPage = PageUriLocator.GetUri<MainPage>();
Have fun with it and till next time.
We've patched this a little bit, because in some Files, after the first 0x00,0x00,0x10 there is not not app.xaml, no there is other data. So we search for app.xaml!
BeantwoordenVerwijderenhere is our modified Version:
https://gist.github.com/jogibear9988/5994727