C
C#3mo ago
popcorn

Fluent Syntax - generic methods

Hi, I have a structure as follows:
public abstract class Control { }

public sealed class Image : Control {
public Image WithUrl(string url) { Url = url; return this; }
public Image WithZoomFactor(float zoomFactor) { ZoomFactor = zoomFactor; return this; }
}

public sealed class Label : Control {
public Label WithText(string text) { Text = text; return this; }
}

public class StackPanel: Control {
public void AddChild(Control control) { }
}

public class Canvas: Control {
public void AddChild(Control control, Point point) { }
}
public abstract class Control { }

public sealed class Image : Control {
public Image WithUrl(string url) { Url = url; return this; }
public Image WithZoomFactor(float zoomFactor) { ZoomFactor = zoomFactor; return this; }
}

public sealed class Label : Control {
public Label WithText(string text) { Text = text; return this; }
}

public class StackPanel: Control {
public void AddChild(Control control) { }
}

public class Canvas: Control {
public void AddChild(Control control, Point point) { }
}
and I want to be able to do :
new Image()
.WithZoomFactor(0.75f)
.PlacedIn(canvas1).At(0, 0)
.WithUrl("flower2.jpg");

new Image()
.WithZoomFactor(0.75f)
.PlacedIn(stackPanel2)
.WithUrl("flower1.jpg");

new Label()
.WithText("Dandelion (lat. Taraxacum officinale)")
.PlacedIn(canvas2).At(50, 200);

// This should not compile - PlaceIn() is in canvas and is not followed by .At()..
// new Image().PlacedIn(canvas2).WithUrl("flower2.jpg");
new Image()
.WithZoomFactor(0.75f)
.PlacedIn(canvas1).At(0, 0)
.WithUrl("flower2.jpg");

new Image()
.WithZoomFactor(0.75f)
.PlacedIn(stackPanel2)
.WithUrl("flower1.jpg");

new Label()
.WithText("Dandelion (lat. Taraxacum officinale)")
.PlacedIn(canvas2).At(50, 200);

// This should not compile - PlaceIn() is in canvas and is not followed by .At()..
// new Image().PlacedIn(canvas2).WithUrl("flower2.jpg");
So the rule is that the method .PlacedIn(canvas) must be either last to be called or must be followed by .At(). And .PlacedIn(stackPanel) can be anywhere and followed by any method. The second problem I solved quite easily by:
public static T PlacedIn<T>(this T control, StackPanel stackPanel) where T : Control {
return control;
}
public static T PlacedIn<T>(this T control, StackPanel stackPanel) where T : Control {
return control;
}
But I'm not sure about the .PlacedIn(canvas). I think it should return some other type than the generic T where : Control but then the information about the type is lost and I don't know how to return the proper type (Label, Image, ...) In the .At() method afterwards. How can I do this?
16 Replies
exokem
exokem3mo ago
If you definitely want the syntax to be PlacedIn(canvas).At(0, 0) You can have the placedin overload accepting a canvas return a separate builder type with an At method that returns the original builder Otherwise it would be simpler to have the position be optional parameters I can't think of another way to absolutely restrict the usage
popcorn
popcorn3mo ago
Oh right I can have the .At() method in the builder 🤦‍♂️ I was still thinking about the method extensions and just didn't know how to return the generic Type in there. You basically meant something like this, right?
public static PositionableControl<T> PlacedIn<T>(this T control, Canvas canvas) where T : Control { }

public class PositionableControl<T> where T : Control {
public T At(int x, int y) { }
}
public static PositionableControl<T> PlacedIn<T>(this T control, Canvas canvas) where T : Control { }

public class PositionableControl<T> where T : Control {
public T At(int x, int y) { }
}
exokem
exokem3mo ago
Yes Only the extension may need to create an instance of positionable control wrapping the original control in order for At to return it
popcorn
popcorn3mo ago
Thanks, I'll try to implement the details and see if it works. Exactly
exokem
exokem3mo ago
If it doesn't work I still think it makes more sense to just have the x and y be parameters for the specific placed at overload Simpler overall
canton7
canton73mo ago
You can do a lot with interfaces as well. Have the same underlying type (and so the same instance), but implement a bunch of interfaces with different methods, that return other interface types On mobile - I can type out an example later
exokem
exokem3mo ago
That would also work, maybe better No need for wrapping that way
popcorn
popcorn3mo ago
Hmm, I dont see how it could be done using interfaces? Also some type of generic parameter at interface?
canton7
canton73mo ago
Give me 15 mins, and I'll be at a computer I can type into Have I understood this right?
public class Canvas { }
public class StackPanel { }

public interface IAt<T>
{
T At(int x, int y);
}

public class Image : IAt<Image>
{
public Image WithUrl(string url) { return this; }
public Image WithZoomFactor(float zoomFactor) { return this; }
public IAt<Image> PlacedIn(Canvas canvas) { return this; }
public Image PlacedIn(StackPanel stackPanel) { return this; }

Image IAt<Image>.At(int x, int y) { return this; }
}
public class Canvas { }
public class StackPanel { }

public interface IAt<T>
{
T At(int x, int y);
}

public class Image : IAt<Image>
{
public Image WithUrl(string url) { return this; }
public Image WithZoomFactor(float zoomFactor) { return this; }
public IAt<Image> PlacedIn(Canvas canvas) { return this; }
public Image PlacedIn(StackPanel stackPanel) { return this; }

Image IAt<Image>.At(int x, int y) { return this; }
}
popcorn
popcorn3mo ago
Right, I get it now. Neat solution Thanks a lot
canton7
canton73mo ago
Sweet. You can take that approach quite far: https://github.com/canton7/Stylet/blob/master/Stylet/StyletIoC/FluentInterface.cs Of course, someone can still type new Image().PlacedIn(canvas) and leave it at that Fluent interfaces are often used for builders, for partially that reason: you can ensure that you only return an instance once the user has reached a particular point in the graph of allowed methods
popcorn
popcorn3mo ago
But that up to the implementation of the .PlacedIn(canvas) to do "nothing" if it's not followed by the .At() method So that we ensure that every Control placed in the canvas has coordinates
canton7
canton73mo ago
So if you do Image.WithUrl(...) etc, you can make sure that you only return an Image instance once the user has called PlacedIn and WithUrl, for instance Yeah, but that's a bit of a footgun. The user said to place in a canvas, so why isn't it appearing in the canvas?
popcorn
popcorn3mo ago
Oh, so this is more of a bad design decision then?
canton7
canton73mo ago
I'd be tempted to either 1) specify the coordinates in the PlacedIn call -- then they can't be forgotten, or 2) do what most other frameworks do and assume (0, 0) if the coordinates aren't specified I guess there's nothing in your system which forces the user to actually finish the builder -- they don't need the Image instance for anything later. So there's no way to force them to get as far as building an Image instance
popcorn
popcorn3mo ago
That's right. Well thank you for your time and help. Appreciate it