WPF provides some types for representing geometrical concepts. The simplest one is Point, which is roughly like this:
public struct Point { public double X { get; set; } public double Y { get; set; } }
This is fine but it misses an important fact about the X and the Y fields, which is that if you tip your head over 90 degrees and look at your monitor sideways, the X and the Y swap over. Which is which is merely a matter of your point of view. And so it is frequently useful to support exactly the same operations in both dimensions, which means that you have to write very nearly exactly the same code twice.
So there are some symmetries hiding in the way WPF represents graphics, and it doesn't have a built in way of abstracting over those symmetries, and the result is code duplication. Not good.
What is needed is a way to write an algorithm that operates on a Point but can be told which dimension to mess with. So we need a variable that identifies a dimension. Simple enough:
public enum Dimension { X, Y }
Then we need some operations we can perform on a point's dimensions. Extension methods are very nice for this:
public double Get(this Point point, Dimension dimension) { if (dimension == Dimension.X) return point.X; return point.Y; }
So now we can retrieve the X coordinate of a point p like this:
double x = p.Get(Dimension.X);
I know this looks a lot uglier than:
double x = p.X;
but it has the major advantage that you can now write, in a fairly natural way, a big complicated algorithm that examines either the X or the Y coordinate of a lot of Points, and you can decide which by passing it a parameter.
What else? Some times my algorithm works mostly along one dimension but then needs to do something to the orthogonal dimension (i.e. the other one of the two available). So this operation is useful:
public Dimension GetOrthogonal(this Dimension dimension) { if (dimension == Dimension.X) return Dimension.Y; return Dimension.X; }
It's like a simple mathematical operator. Maybe group theory is relevant here, though probably not worth digging into right now.
A more pressing problem is that of setting the values. The big problem is that extension methods are quite limited here. An extension method on a value type is passed a copy of the object, because it's a value type. And this isn't allowed:
public void Set(this ref Point p, Dimension dim, double val)
The compiler won't let us put the ref modifier on a this parameter. There was probably some important reason for this involving generics, but it's a real pain in most situations. But we can still use an extension method in a different way:
public void Set(this Dimension dim, ref Point p, value val) { if (dim == Dimension.X) p.X = val; else p.Y = val; }
In other words, the Dimension has the setter on it, rather than the Point. So:
Point p;
Dimension d = Dimension.X;
d.Set(ref p, 10);
Nice'n'easy, although it looks ugly when the Point is a property of something, because you have to copy it out into a temporary variable and then modify it, and finally copy it back into the property. But that's value-type properties for you. One of the few ugly wrinkles of C#.
Of course, the real solution is to not write functions that have side-effects on value-type parameters. Or to put it another way:
public Point GetAdjusted(this Point p, Dimension dim, double val) { if (dim == Dimension.X) p.X = val; else p.Y = val; return p; }
So to change Dimension d of a Point p to 56.1, we would write:
p = p.GetAdjusted(d, 65.1);
Which ain't too bad. In reality, I provide extensions both for Point and Dimension so I can use the least-painful variation depending on the situation.
A similar facility can help with using Rect, which has its own special problems. It has these among its properties:
public double Left { get; } public double Top { get; } public double Right { get; } public double Bottom { get; } public double Width { get; set; } public double Height { get; set; } public double X { get; set; } public double Y { get; set; }
Note that the first four can't be set, which seems unnecessarily incomplete. Anyway, just as a Point has two dimensions, a Rect has four sides:
public enum Side { Left, Top, Right, Bottom }
As well as the same operations for getting and setting the positions of the sides on a given Rect, there's this handy operation that gives you the opposite of a side:
public Side GetOpposite(this Side side) { switch (side) { case Side.Left: return Side.Right; case Side.Right: return Side.Left; case Side.Top: return Side.Bottom; } return Side.Top; }
And how about moving between the worlds of sides and dimensions? To get from a side to the corresponding dimension:
public Dimension GetDimension(this Side side) { switch (side) { case Side.Left: return Dimension.X; case Side.Right: return Dimension.X; case Side.Top: return Dimension.Y; } return Dimension.Y; }
Moving the other way presents a choice because each dimension has two sides:
public Side GetMinimumSide(this Dimension dim) { if (dim == Dimension.X) return Side.Left; return Side.Right; } public Side GetMaximumSide(this Dimension dim) { if (dim == Dimension.X) return Side.Right; return Side.Bottom; }
Nothing too complicated. I'll post a real example use soon.
No comments:
Post a Comment