1

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.

Harry
  • 4,524
  • 4
  • 42
  • 81

1 Answers1

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;
}
Léo SALVADOR
  • 754
  • 2
  • 3
  • 17
  • Alas, this doesn't solve the problem, it's way more complex than this. My app generates a local NuGet repository. Where? It takes the location from the JSON file. Where is the JSON file? It's relative to the main executable. Then the path in configuration is also relative. It's resolved relative to main executable. Why? To make it work when the solution is placed anywhere. So - the path for the designer must be relative or it won't work. I could just leave the view blank in the designer, but I'm just stubborn ;) – Harry Jan 21 '22 at 22:22
  • And isn't it possible to simulate more basic behavior for the Designer only (with an example like the one given)? What do you expect from the Designer? Is displaying test data to visualize the behavior and style of the different Controls not enough? – Léo SALVADOR Jan 21 '22 at 23:09
  • It is, I could mock the structure, but I'm looking for a more advanced solution. Just in case. The ugly hack works for now, I polished it a little and it does the job. So the main question remains - how to get the directory? I've found it in only one place - debug symbols present in cached executable. But maybe it's somewhere else too. – Harry Jan 21 '22 at 23:17
  • 1
    I have a solution, I will update my answer, tell me if it works. – Léo SALVADOR Jan 22 '22 at 00:55
  • That looks interesting! It would remove the ugliest part of my hack with throwing the exception. I'll test that later today. It seems like you nailed it. – Harry Jan 22 '22 at 12:59
  • I've tested it. Works perfectly! Later I will post the complete code with the application. – Harry Jan 22 '22 at 13:28