How to get the application directory in WPF designer, .NET 6.0?

My WPF application reads a directory structure from a location relative to the main application executable.

Then it visualizes the directory structure in a TreeView.

It works fine when I just run the application, however, when I preview it in the XAML designer, it does not work. I found it's loaded from a different location like this:

C:\Users\Adam\AppData\Local\Microsoft\VisualStudio\17.0_108779a0\Designer\Cache\1481370356x64DA\

Yes, it's the value of the System.AppDomain.CurrentDomain.BaseDirectory property.

I get similar location from assembly.Location.

I know there are similar questions asked a long time ago with answers valid for previous Visual Studio versions and previous .NET versions. None of them works for .NET 6 and Visual Studio 2022. Please kindly do not mark this as duplicate of those questions.

To verify those answers don't work with .NET 6 - just create a new .NET 6 WPF project, in any code executed by a view insert following code:

throw new Exception($"Path: {System.AppDomain.CurrentDomain.BaseDirectory}");

Reload the designer, and you will see what am I talking about.

I already found a very ugly workaround, but it just hhhhhhideous! When I intentionally throw and catch an exception. I will get a path to my code in the stack trace. Then I can extract my project directory from there, then find the output path in the project file and that's it. But come on! There must be a cleaner way!

UPDATE: Here's the hhhhhhideous hack:

using System.IO;
using System.Reflection;
using System.Xml;

static class Abominations {

    /// <summary>
    /// Gets the calling project's output directory in a hideous way (from debug information). Use when all else fails.
    /// </summary>
    /// <param name="action">Action that throws an exception.</param>
    /// <returns>Calling project's output directory.</returns>
    public static string GetCallingProjectOutputDirectory(Action action) {
        try {
            action();
        }
        catch (Exception exception) {
            var stacktrace = exception.StackTrace!.Split(Environment.NewLine).First();
            var p1 = stacktrace.IndexOf(" in ") + 4;
            var p2 = stacktrace.IndexOf(":line");
            var pathLength = p2 - p1;
            if (p1 < 0 || p2 < 0 || pathLength < 1) throw new InvalidOperationException("No debug information");
            var callingSource = stacktrace[p1..p2];
            var directory = new DirectoryInfo(Path.GetDirectoryName(callingSource)!);
            FileInfo? projectFile;
            do {
                projectFile = directory.GetFiles("*.csproj").FirstOrDefault();
                if (projectFile is null) directory = directory.Parent!;
            }
            while (projectFile is null);
            var projectXml = new XmlDocument();
            projectXml.Load(projectFile.FullName);
            var baseOutputPath = projectXml.GetElementsByTagName("BaseOutputPath").OfType<XmlElement>().FirstOrDefault()?.InnerText;
            var outputDirectory = directory.FullName;
            outputDirectory = baseOutputPath is not null
                ? Path.GetFullPath(Path.Combine(outputDirectory, baseOutputPath))
                : Path.Combine(outputDirectory, "bin");
            var buildConfiguration =
                Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>()!.Configuration;
            var targetFramework =
                projectXml.GetElementsByTagName("TargetFramework").OfType<XmlElement>().FirstOrDefault()!.InnerText;
            outputDirectory = Path.Combine(outputDirectory, buildConfiguration, targetFramework);
            return outputDirectory;
        }
        throw new InvalidOperationException("Provided action should throw");
    }

}

I tested it and it works. But it's just an atrocious abomination and something that should be killed with fire.


Solution 1:

Assuming you have a DataContext class like this:

public class ViewModel
{
    public string Path { get; set; }
    public ViewModel()
    {
        Path = AppDomain.CurrentDomain.BaseDirectory;
    }
}

If you do a Binding on this Datacontext, for example like this:

<Grid>
    <TextBlock Text="{Binding Path}"></TextBlock>
</Grid>

Indeed, a different path is found between the Designer and the Runtime. Here is a solution that generally works for this type of problem:

Create a class derived from your DataContext class, and set a test value (only valid for the Designer context):

public class DesignViewModel : ViewModel
{
    public DesignViewModel()
    {
        Path = "Path for Designer only";
    }
}

And then, use this class to set the Designer Datacontext:

d:DataContext="{d:DesignInstance Type=local:DesignViewModel, IsDesignTimeCreatable=True}"

It's a way to work around the problem by forcing the value you want for the Designer.

UPDATE

If you need to retrieve the path at the time of compile (instead of the Design Time), the CallerFilePathAttribute could be interesting.

Example:

public static string GetCompilationPath([System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "")
{
    return sourceFilePath;
}