Today we are going to show a partial implementation of the fractal processing example we described last time. This should give a feeling of how typical code written above the notification server framework looks like.
We skip the RequestListener
implementation for now. We will get to it in another time. What is important though is the event that the RequestListener produces. As explained in the first post, events are messages that flow through the server’s components. In our case, the RequestListener
will produce events of type FractalRequestEvent
, which have the following form:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class FractalRequestEvent : IEvent
{
// Dimensions of the produced bitmap
public int Width { get; set; }
public int Height { get; set; }
// The following four parameters describe the rectangular domain in the complex plane
// we are interested in plotting
// Left <= x <= Right
public double Left { get; set; }
public double Right { get; set; }
// Bottom <= y <= Top
public double Top { get; set; }
public double Bottom { get; set; }
// The maximum number of iterations to run the algorithm per point
public int MaxNumberOfIterations { get; set; }
// The file path to save the generated image to
public string FilePath { get; set; }
}
|
Some remarks:
- Events are classes that hold data and should avoid implementing logic. These remind of records, recently introduced in C# 9.0, and it would be interesting to see a notification server supporting record types. We might discuss this in a future post.
- The convention is that events should have the suffix Event. Unfortunately, this convention was not always followed, but we will try to follow it in this blog.
- Events implement the
IEvent
interface. We will not explain about this interface here. This will be done in a post dedicated to IEvent
. In fact, our current event does not technically implement the interface, but we ignore this here.
We introduce two events for fractal requests: one for Mandelbrot fractals and the other for Julia fractals:
1
2
3
4
5
6
7
8
| public class MandelbrotRequestEvent : FractalRequestEvent
{
}
public class JuliaRequestEvent : FractalRequestEvent
{
public Complex C { get; set; }
}
|
Note that the JuliaRequestEvent
class has an additional property, which specifies the parameter c
for the Julia set algorithm.
We next move to the implementation of the PixelGeneratorLogic
. This logic receives events of type FractalRequestEvent
and generates many pixel events. We first define the PixelEvent
class:
1
2
3
4
5
6
7
8
9
| public class PixelEvent : IEvent
{
// The coordinates of the pixel in the bitmap
public int X { get; set; }
public int Y { get; set; }
// The request associated with this pixel
public FractalRequestEvent Request { get; set; }
}
|
Then the PixelGeneratorLogic
generates these pixels according to the received request’s Width
and Height
properties:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class PixelGeneratorLogic : Logic
{
public override IEvent ProcessEvent(IEvent eventToProcess)
{
FractalRequestEvent request = eventToProcess as FractalRequestEvent;
EventGroup result = new EventGroup();
for (int x = 0; x < request.Width; x++)
{
for (int y = 0; y < request.Height; y++)
{
var currentPixel = new PixelEvent()
{
Request = request,
X = x,
Y = y
};
result.Add(currentPixel);
}
}
return result;
}
}
|
Here we see that PixelGeneratorLogic
inherits from the class Logic
and overrides its method ProcessEvent
. The method ProcessEvent
receives as input an instance of type IEvent
, and we would have null in this method if we received an event of a different type. This will be discussed in a later post. We also see that we return from this method a type named EventGroup
. This type is a collection of events, and it is also an IEvent
itself! This will also be discussed later.
The next component we describe is PixelToComplexConvertLogic
. It produces an event which contains in addition to the pixel coordinates, the complex number it represents. We use composition this time instead of inheritance:
1
2
3
4
5
6
7
8
| public class ComplexPixelEvent : IEvent
{
// The number in the complex plane represented by this pixel
public Complex ComplexNumber { get; set; }
// The original pixel
public PixelEvent Pixel { get; set; }
}
|
And the implementation of the logic is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| public class PixelToComplexConvertLogic : Logic
{
public override IEvent ProcessEvent(IEvent eventToProcess)
{
PixelEvent pixelEvent = eventToProcess as PixelEvent;
ComplexPixelEvent result = new ComplexPixelEvent()
{
Pixel = pixelEvent,
ComplexNumber = ComputeComplexNumber(pixelEvent)
};
return result;
}
private Complex ComputeComplexNumber(PixelEvent pixelEvent)
{
FractalRequestEvent request = pixelEvent.Request;
double x = ComputeCoordinate(pixelEvent.X, request.Width, request.Left, request.Right);
double y = ComputeCoordinate(pixelEvent.Y, request.Height, request.Top, request.Bottom);
return new Complex(x, y);
double ComputeCoordinate(int pixelCoordinate, int pixelMax, double start, double end)
{
return start + ((double) pixelCoordinate) / (pixelMax - 1) * (end - start);
}
}
}
|
Next, we implement the Mandelbrot and Julia algorithms. These are pretty similar to PixelToComplexConvertLogic
, but much more complicated. I omit the implementation here. You can find it here. I will only specify the produced event:
1
2
3
4
5
6
7
8
| public class IteratedPixelEvent : IEvent
{
// Number of iterations done on this pixel
public int Iterations { get; set; }
// The original pixel
public PixelEvent Pixel { get; set; }
}
|
I also want to discuss the rule implementation. As noted in the previous post, we want events to be filtered to MandelbrotLogic
or JuliaLogic
according to the received pixel’s Request
type. This is done by implementing a rule:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class MandelbrotRule : IRule
{
public bool IsActivated(IEvent eventToCheck)
{
ComplexPixelEvent pixelEvent = eventToCheck as ComplexPixelEvent;
if (pixelEvent != null)
{
return pixelEvent.Pixel.Request is MandelbrotRequestEvent;
}
return false;
}
}
public class JuilaRule : IRule
{
public bool IsActivated(IEvent eventToCheck)
{
ComplexPixelEvent pixelEvent = eventToCheck as ComplexPixelEvent;
if (pixelEvent != null)
{
return pixelEvent.Pixel.Request is JuliaRequestEvent;
}
return false;
}
}
|
As illustrated in these implementations, we only need to implement the method IsActivated
, which checks whether the given event passes filter (and should be received by the next component) or not.
Next, we need to implement ColorConvertLogic
. Again, this looks like the previous logics, and I omit the implementation. You can find an example of a simple gray scale implementation here. This logic produces events of the following form:
1
2
3
4
5
6
7
8
| public class ColoredPixelEvent : IEvent
{
// The pixel's color
public Color Color { get; set; }
// The original pixel
public PixelEvent Pixel { get; set; }
}
|
We now move to PixelStoreLogic
. This logic is quite different than the others and more interesting. It receives events both from RequestListener
and ColorConvertLogic
. It stores colored pixels from ColorConvertLogic
and publishes them as a single event once it received all pixels. We assume that the class is single threaded, and give the following implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| public class ImageEvent : IEvent
{
// The pixels of this image
public ICollection<ColoredPixelEvent> Pixels { get; set; }
// The request associated to this image
public FractalRequestEvent Request { get; set; }
}
public class PixelStoreLogic : Logic
{
private readonly IDictionary<FractalRequestEvent, ICollection<ColoredPixelEvent>>
_requestToPixels = new Dictionary<FractalRequestEvent, ICollection<ColoredPixelEvent>>();
public override IEvent ProcessEvent(IEvent eventToProcess)
{
switch (eventToProcess)
{
case FractalRequestEvent requestEvent:
ProcessFractalRequest(requestEvent);
break;
case ColoredPixelEvent coloredPixel:
ProcessColoredPixel(coloredPixel);
break;
}
return null;
}
private void ProcessFractalRequest(FractalRequestEvent requestEvent)
{
_requestToPixels[requestEvent] = new List<ColoredPixelEvent>();
}
private void ProcessColoredPixel(ColoredPixelEvent coloredPixel)
{
FractalRequestEvent request = coloredPixel.Pixel.Request;
ICollection<ColoredPixelEvent> requestPixels = _requestToPixels[request];
requestPixels.Add(coloredPixel);
if (requestPixels.Count == request.Height * request.Width)
{
_requestToPixels.Remove(request);
var eventToPublish =
new ImageEvent()
{
Pixels = requestPixels,
Request = request
};
Publish(eventToPublish);
}
}
}
|
There are some interesting things here. We always return null
from this ProcessEvent
implementation. When a logic returns null
from its ProcessEvent
implementation, the notification server framework does not pass this value to the components linked to it. But somehow events do get published to the linked components. This is done by the call to the Publish
method. Any component (except for target components) can publish events manually by calling the Publish
method.
Another interesting thing here is the use of C# 7.0 pattern matching. Those of you who are familiar with the notification server framework are used to see to this sort of pattern using a special class called ProcessorLogic
which is built-in in the framework.
We proceed to the last two remaining components. The implementation of BitmapConvertLogic
is pretty similar to the other logic implementations we’ve seen before.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class BitmapEvent : IEvent
{
public Bitmap Bitmap { get; set; }
public FractalRequestEvent Request { get; set; }
}
public class BitmapConvertLogic : Logic
{
public override IEvent ProcessEvent(IEvent eventToProcess)
{
ImageEvent imageEvent = eventToProcess as ImageEvent;
FractalRequestEvent request = imageEvent.Request;
Bitmap bitmap = new Bitmap(request.Width, request.Height);
foreach (ColoredPixelEvent coloredPixel in imageEvent.Pixels)
{
PixelEvent pixel = coloredPixel.Pixel;
bitmap.SetPixel(pixel.X, pixel.Y, coloredPixel.Color);
}
return new BitmapEvent
{
Bitmap = bitmap,
Request = request
};
}
}
|
We now move to discuss FileSystemDispatcher
. There is not too much to discuss here, the only thing is that the method signature is slightly different:
1
2
3
4
5
6
7
8
9
10
| public class FileSystemDispatcher : Dispatcher
{
public override void DoDispatch(EventGroup eventGroup)
{
foreach (BitmapEvent bitmapEvent in eventGroup.OfType<BitmapEvent>())
{
bitmapEvent.Bitmap.Save(bitmapEvent.Request.FilePath, ImageFormat.Png);
}
}
}
|
The idea of this method signature is that there might be a more efficient way to dispatch a group of events together at the same time instead of dispatching each one individually. We will also talk about this again later.
This is it for now. You might wonder how do we link these components together and test this? This will be explained next time.