8
\$\begingroup\$

I want to make the creation of the pack Uri for WPF resources more verbose so they are easier to read and less error-prone and I thought I implement it using the builder pattern.


The base class for each builder (there are four of them) is the PackUriBuilder. It implements the UriKind logic, checks the path and requries the derived class to implement the AbsolutePath property. It also provides static methods to initialize each of the builders.

(I omitted null checks to make is shorter).

abstract class PackUriBuilder
{
    private string _path;
    private UriKind _uriKind;

    protected PackUriBuilder(string path)
    {
        if (!path.StartsWith("/")) throw new ArgumentException(paramName: nameof(path), message: "Path must start with \"/\".");
        _path = path;
        _uriKind = UriKind.Absolute;
    }

    public string Scheme => "pack";

    public string RelativePath => _path;

    public abstract string AbsolutePath { get; }

    public static implicit operator string (PackUriBuilder builder) => builder.ToString();

    public static implicit operator Uri(PackUriBuilder builder)
    {
        switch (builder._uriKind)
        {
            case UriKind.Absolute:
                return new Uri(builder.ToString(), UriKind.Absolute);
            case UriKind.Relative:
                return new Uri(builder.RelativePath, UriKind.Relative);
        }
        // todo: I know this needs a better message ;-)
        throw new ArgumentOutOfRangeException("Invalid UriKind.");
    }

    public override string ToString()
    {
        switch (_uriKind)
        {
            case UriKind.Absolute:
                return AbsolutePath;
            case UriKind.Relative:
                return RelativePath;
        }
        // todo: I know this needs a better message ;-)
        throw new ArgumentOutOfRangeException("Invalid UriKind.");
    }

    public Uri ToUri() => (Uri)this;

    public PackUriBuilder Relative()
    {
        _uriKind = UriKind.Relative;
        return this;
    }

    public PackUriBuilder Absolute()
    {
        _uriKind = UriKind.Absolute;
        return this;
    }

    public static LocalAssemblyResourceFilePackUriBuilder LocalAssemblyResourceFile(string path)
    {
        return new LocalAssemblyResourceFilePackUriBuilder(path);
    }

    public static ReferencedAssemblyResourceFilePackUriBuilder ReferencedAssemblyResourceFile(string path, string assemblyShortName)
    {
        return new ReferencedAssemblyResourceFilePackUriBuilder(path, assemblyShortName);
    }

    public static ContentFilePackUriBuilder ContentFile(string path)
    {
        return new ContentFilePackUriBuilder(path);
    }

    public static SiteOfOriginPackUriBuilder SiteOfOrigin(string path)
    {
        return new SiteOfOriginPackUriBuilder(path);
    }
}

These are the four builders for each pack Uri type.

class LocalAssemblyResourceFilePackUriBuilder : PackUriBuilder
{
    public LocalAssemblyResourceFilePackUriBuilder(string path) 
    : base(path) { }

    public override string AbsolutePath
    {
        get
        {
            return $"{Scheme}://application:,,,{RelativePath}";
        }
    }
}

class ReferencedAssemblyResourceFilePackUriBuilder : PackUriBuilder
{
    private string _assemblyShortName;
    private string _version;
    private string _publicKey;

    public ReferencedAssemblyResourceFilePackUriBuilder(string path, string assemblyShortName) 
    : base(path)
    {
        _assemblyShortName = assemblyShortName;
    }

    public ReferencedAssemblyResourceFilePackUriBuilder Version(string version)
    {
        _version = version;
        return this;
    }

    public ReferencedAssemblyResourceFilePackUriBuilder PublicKey(string publicKey)
    {
        _publicKey = publicKey;
        return this;
    }

    public override string AbsolutePath
    {
        get
        {
            var version = string.IsNullOrEmpty(_version) ? string.Empty : $";{_version}";
            var publicKey = string.IsNullOrEmpty(_publicKey) ? string.Empty : $";{_publicKey}";
            return $"{Scheme}://application:,,,/{_assemblyShortName}{version}{publicKey};component{RelativePath}";
        }
    }
}

class ContentFilePackUriBuilder : PackUriBuilder
{
    public ContentFilePackUriBuilder(string path) 
    : base(path) { }

    public override string AbsolutePath
    {
        get
        {
            return $"{Scheme}://application:,,,{RelativePath}";
        }
    }
}

class SiteOfOriginPackUriBuilder : PackUriBuilder
{
    public SiteOfOriginPackUriBuilder(string path) 
    : base(path) { }

    public override string AbsolutePath
    {
        get
        {
            return $"{Scheme}://siteoforigin:,,,{RelativePath}";
        }
    }
}

Examples

// LINQPad requires this because it's not running in WPF context.
if (!UriParser.IsKnownScheme("pack")) { new System.Windows.Application(); }

PackUriBuilder.LocalAssemblyResourceFile("/test.bmp").ToString().Dump();
PackUriBuilder.ReferencedAssemblyResourceFile("/test.bmp", "MyAssembly").Version("v2.3.0.1").ToString().Dump();
PackUriBuilder.SiteOfOrigin("/test.bmp").Relative().ToString().Dump();

Output:

pack://application:,,,/test.bmp
pack://application:,,,/MyAssembly;v2.3.0.1;component/test.bmp
/test.bmp
\$\endgroup\$

3 Answers 3

9
\$\begingroup\$

Your code looks pretty got but there are some things that you can simplify, probably the switch cases are the most unwanted part of your code.

Verbatim strings

"Path must start with \"/\"."

Can become

@"Path must start with ""/""."

get only properties

private string _path;
public string RelativePath => _path;

Can become

public string RelativePath { get; }

get only properties, without a backing field

You can use expression bodies for all of your get only properties, without a backing field:

public override string AbsolutePath
{
    get
    {
        return $"{Scheme}://application:,,,{RelativePath}";
    }
}

Can become

public override string AbsolutePath => $"{Scheme}://siteoforigin:,,,{RelativePath}";

You can do the same for all of the AbsolutePath properties.

private string _assemblyShortName; can be a readonly variable

Redundant casts

public Uri ToUri() => (Uri) this;

You already have an implicit operator that does that for you

public static implicit operator Uri(PackUriBuilder builder)

Replacing the nasty switch case with a Dictionary

public static implicit operator Uri(PackUriBuilder builder)
{
    switch (builder._uriKind)
    {
        case UriKind.Absolute:
            return new Uri(builder.ToString(), UriKind.Absolute);
        case UriKind.Relative:
             return new Uri(builder.RelativePath, UriKind.Relative);
    }
        // todo: I know this needs a better message ;-)
   throw new ArgumentOutOfRangeException("Invalid UriKind.");
}

With a dictionary looking similar to this one

protected static Dictionary<UriKind, Func<PackUriBuilder, Uri>> helper = new Dictionary
        <UriKind, Func<PackUriBuilder, Uri>>
        {
            {UriKind.Absolute, builder => new Uri(builder.ToString(), UriKind.Absolute)},
            {UriKind.Relative, builder => new Uri(builder.RelativePath, UriKind.Relative)}
        };

Can become

public static implicit operator Uri(PackUriBuilder builder) => helper[builder._uriKind].Invoke(builder);

Your public override string ToString() is similar but it might be a little bit more harder to do with a dictionary, since there are some virtual variables involved, namely AbsolutePath and since you can't make it into a static variable and initialize it like the helper dictionary used for the implicit operator, You might need to initialize the dictionary from the constructor but as you know virtual calls can be very tricky if called in the constructor.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ I guess there is not much more to improve. I'm embarassed as usually I write similar reviews but when you look at your own code you have sometimes a blind spot. \$\endgroup\$
    – t3chb0t
    Dec 24, 2016 at 13:24
  • \$\begingroup\$ I completely agree happens to me all the time too.. \$\endgroup\$
    – Denis
    Dec 24, 2016 at 13:26
5
\$\begingroup\$

You're likely to run across 101 different little gotchyas by rolling your own here. For example, you're not escaping any invalid characters in your solution.

I would build your implementation on top of the existing UriBuilder class. By using it internally, I'm certain you'll save yourself many headaches down the road.

\$\endgroup\$
3
  • \$\begingroup\$ This is a good idea. Makes a lot of things easier. But I think it's still better then typing those strings by hand all the time. I rarely get them right at the first time. \$\endgroup\$
    – t3chb0t
    Dec 24, 2016 at 12:31
  • \$\begingroup\$ Absolutely a worthwhile venture @t3chb0t!! I just wrote a similar little library for building SSRS report URLs about 2 weeks ago. The team loves it! \$\endgroup\$
    – RubberDuck
    Dec 24, 2016 at 12:32
  • 1
    \$\begingroup\$ I'm afraid my excitement was premature ;-( I tried it out and checked the source code for the UriBuilder and unfortunatelly it doesn't validate anything that would be useful in WPF context like the path needs to start with a slash or the % and ? need to be escaped. The uri it produces is not of much value for WPF resources either. I think I'll add regex here and there and it'll do. \$\endgroup\$
    – t3chb0t
    Dec 24, 2016 at 12:46
1
\$\begingroup\$

In case someone wanted to use the improved version here's the final one.

Credits go to @denis.

abstract class PackUriBuilder
{
    private static readonly Dictionary<UriKind, Func<PackUriBuilder, Uri>> UriFactories = new Dictionary<UriKind, System.Func<PackUriBuilder, Uri>>
    {
        [UriKind.Absolute] = builder => new Uri(builder.AbsolutePath, UriKind.Absolute),
        [UriKind.Relative] = builder => new Uri(builder.RelativePath, UriKind.Relative),
    };

    private static readonly Dictionary<UriKind, Func<PackUriBuilder, string>> StringFactories = new Dictionary<UriKind, System.Func<PackUriBuilder, string>>
    {
        [UriKind.Absolute] = builder => builder.AbsolutePath,
        [UriKind.Relative] = builder => builder.RelativePath,
    };

    private UriKind _uriKind;

    protected PackUriBuilder(string path)
    {
        if (!path.StartsWith("/"))
        {
            throw new ArgumentException(paramName: nameof(path), message: @"Path must start with ""/"".");
        }
        RelativePath = path;
        _uriKind = UriKind.Absolute;
    }

    public string Scheme => "pack";

    public string RelativePath { get; }

    public abstract string AbsolutePath { get; }

    public static implicit operator string (PackUriBuilder builder) => builder.ToString();

    public static implicit operator Uri(PackUriBuilder builder) => UriFactories[builder._uriKind](builder);

    public override string ToString() => StringFactories[_uriKind](this);   

    public Uri ToUri() => this;

    public PackUriBuilder Relative()
    {
        _uriKind = UriKind.Relative;
        return this;
    }

    public PackUriBuilder Absolute()
    {
        _uriKind = UriKind.Absolute;
        return this;
    }

    public static LocalAssemblyResourceFilePackUriBuilder LocalAssemblyResourceFile(string path)
    {
        return new LocalAssemblyResourceFilePackUriBuilder(path);
    }

    public static ReferencedAssemblyResourceFilePackUriBuilder ReferencedAssemblyResourceFile(string path, string assemblyShortName)
    {
        return new ReferencedAssemblyResourceFilePackUriBuilder(path, assemblyShortName);
    }

    public static ContentFilePackUriBuilder ContentFile(string path)
    {
        return new ContentFilePackUriBuilder(path);
    }

    public static SiteOfOriginPackUriBuilder SiteOfOrigin(string path)
    {
        return new SiteOfOriginPackUriBuilder(path);
    }
}

And the builders:

class LocalAssemblyResourceFilePackUriBuilder : PackUriBuilder
{
    public LocalAssemblyResourceFilePackUriBuilder(string path) : base(path) { }

    public override string AbsolutePath => $"{Scheme}://application:,,,{RelativePath}";
}

class ReferencedAssemblyResourceFilePackUriBuilder : PackUriBuilder
{
    private readonly string _assemblyShortName;
    private string _version;
    private string _publicKey;

    public ReferencedAssemblyResourceFilePackUriBuilder(string path, string assemblyShortName) : base(path)
    {
        _assemblyShortName = assemblyShortName;
    }

    private string VersionOrEmpty => string.IsNullOrEmpty(_version) ? string.Empty : $";{_version}";

    private string PublicKeyOrEmpty => string.IsNullOrEmpty(_publicKey) ? string.Empty : $";{_publicKey}";

    public ReferencedAssemblyResourceFilePackUriBuilder Version(string version)
    {
        _version = version;
        return this;
    }

    public ReferencedAssemblyResourceFilePackUriBuilder PublicKey(string publicKey)
    {
        _publicKey = publicKey;
        return this;
    }

    public override string AbsolutePath => $"{Scheme}://application:,,,/{_assemblyShortName}{VersionOrEmpty}{PublicKeyOrEmpty};component{RelativePath}";
}

class ContentFilePackUriBuilder : PackUriBuilder
{
    public ContentFilePackUriBuilder(string path) : base(path) { }

    public override string AbsolutePath => $"{Scheme}://application:,,,{RelativePath}";
}


class SiteOfOriginPackUriBuilder : PackUriBuilder
{
    public SiteOfOriginPackUriBuilder(string path) : base(path) { }

    public override string AbsolutePath => $"{Scheme}://siteoforigin:,,,{RelativePath}";
}
\$\endgroup\$
10
  • \$\begingroup\$ I really like the way you made StringFactories work, I forgot you can directly use the instance method, well done ! \$\endgroup\$
    – Denis
    Dec 24, 2016 at 14:17
  • \$\begingroup\$ @denis yeah, the switch was ugly and if there was already one dictionary why not two ;-) \$\endgroup\$
    – t3chb0t
    Dec 24, 2016 at 14:21
  • \$\begingroup\$ Can you link me to the documentation on the square brackets here [UriKind.Absolute] i don't know what they're purpose is in the code \$\endgroup\$ Dec 24, 2016 at 14:34
  • \$\begingroup\$ @downrep_nation here you are Pack URIs in WPF sections "Absolute vs. Relative Pack URIs" & "Programming with Pack URIs" \$\endgroup\$
    – t3chb0t
    Dec 24, 2016 at 14:35
  • \$\begingroup\$ @t3chb0t I think he meant this. [UriKind.Absolute] = builder => new... \$\endgroup\$
    – RubberDuck
    Dec 24, 2016 at 17:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.