Views in Comet
Tags: dotnet, beginners
Published: 2022-01-15 22:02:13

> Comet is a .NET experiment inspired by Flutter and Swift UI for building cross-platform apps with C#.

{% github dotnet/Comet no-readme %}

All UI in Comet is a `View`, which makes things really simple, right? Other toolkits may split them into controls, layouts, components, widgets, etc. That makes sense since some are best used for containing and organizing, others best for accepting user input, and others for creating beautiful UI. 

Let's take a tour of what's available today in Comet and how you can quickly start composing your UI.

## Container Views

The layouts in Comet extend from `ContainerView` which itself is a `View`. These managed views arrange and space child views on the screen, and all adapt to whatever size and density the app runs on. The current layout views include:

* **HStack, VStack, ZStack** - these views will display children horizontally, vertically, or in z-order
* **Grid, HGrid, VGrid** - these views use a powerful column/row system to arrange child views
* **ScrollView** - simply contains a single view that scrolls within the visible viewport

The stack family of views are super simple. You add your children inside them, and set some spacing. Here's the basic content of the Comet template:

```csharp
[Body]
View body()
	=> new VStack {
			new Text(count.ToString()),

			new Button("Increment", ()=>{
				count.Value++;
			})				
	};
```

Each view will accept the minimal set of constructor arguments to be useful. Keeping things simple and efficient is the name of the game. Notice the `VStack` above doesn't require any constructor arguments. Optionally you can include `VStack(HorizontalAlignment alignment = HorizontalAlignment.Center, float? spacing = null)`. 

In C# the children of a view are within the curly braces, comma delimited. The children of our `VStack` are a `Text` and a `Button`. The order is what determines what appears at the top of the screen moving down the screen.

As you can imagine `HStack` does exactly the same only on the horizontal axis. The only different here will be if you're device is configured for a right-to-left (RTL) region. In that case the content will flow from the right instead of the default which is from the left. For this reason, our alignment options use the terms "Start" and "End" instead.

Now, `ZStack` is a bit different. The first child in a `ZStack` will be at the "back" or closest to the parent view surface. The subsequent views will all stack up on top of that view.

```csharp
[Body]
View body()
	=> new ZStack {
			new ShapeView(
				new Circle()
					.Fill(Colors.OrangeRed)
			).Frame(width:80,height:80, alignment: Alignment.Center)
			.Background(Colors.Transparent),
			new ShapeView(
				new RoundedRectangle(2)
					.Stroke(color: Colors.Brown, lineWidth:0)
					.Fill(Colors.Orange)
			).Frame(width:30,height:30, alignment: Alignment.Center)
			.Background(Colors.Transparent)
	}.Background(Color.FromHex("#101010"));
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/he56sezs5stbiodnsgqh.png)

By default children are aligned to the center of `ZStack`. To change alignment, the children's `Frame` takes an alignment argument with many options:

| | | |
| :-- | :-- | :-- |
| TopLeading | Top | TopTrailing |
| Leading | Center | Trailing |
| BottomLeading | Bottom | BottomTrailing |


In order to move this `ZStack` as a group around the screen, I can surround it with another view such as `Grid` and then use the `Alignment` property to move the stack without disturbing the 2 shapes stacked and centered.

```csharp
[Body]
View body()
	=> new Grid{
		new ZStack {
			new ShapeView(
				new Circle()
					.Fill(Colors.OrangeRed)
			).Frame(width:80,height:80)
			.Background(Colors.Transparent),
			new ShapeView(
				new RoundedRectangle(2)
					.Stroke(color: Colors.Brown, lineWidth:0)
					.Fill(Colors.Orange)
			).Frame(width:30,height:30)
			.Background(Colors.Transparent)
		}.Frame(width:80,height:80,alignment:Alignment.Top)
	}.Background(Color.FromHex("#101010"));
```

### The Spacer

One of my favorite parts of layout in Comet is being able to use `Spacer` to boss around the other elements shoving them into the spacing and position I want. This nifty view can take up a specific amount of space by setting the `Frame` just like any other view, and by default it will expand to fill the space available. 

Look at the example of this simple counter. This will display in a vertical view starting from the top of the screen.

```csharp
new VStack(){
	new Text(()=> $"Count is {Count}.")
		.Color(Colors.White)
		.FontSize(64)
		.LineBreakMode(LineBreakMode.WordWrap)
		.HorizontalTextAlignment(TextAlignment.Center)
		.Margin(left:30,right:30),
		new Spacer(),
	new Button("Increment", ()=> Count.Value++)
		.Frame(height:76)
		.FontSize(32)
		.Color(Colors.White)
		.Background(Colors.OrangeRed)
		.RoundedBorder(20)
		.Margin(left:30,right:30)
}
```

If I now add `new Spacer()` before the `Text` it will fill the available space and push the content to the bottom of the view:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3yog6z8xoagd0mabet81.png)

If I put a bookend spacer at the end of the `VStack`, then my content is centered in the view. 😃


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cd4uozp10zklf5bkk1qx.png)

And to complete the story, if I put a spacer in the middle of the elements, they will space evenly. Another way to do this is to use the spacing property on stacks, but I find `Spacer` to be a powerful way to achieve a variety of results.

### Grids

The other major category of layout-base views are the grid family. Anyone with web and css experience has probably use a grid system before, and this may feel quite familiar. Starting with a `Grid` you define the number of columns and rows you want by providing sizing. Column and row indexing are 0 based, so beware the off-by-one gremlin.

Here is a 3x3 Grid with `Buttons` across the middle. I'm using the fill methods just to make the spacing and sizing obvious.

```csharp
new Grid(
    columns: new object[]{ 140, "*", 140},
    rows: new []{"*", "*", "*"},
    defaultRowHeight: 300
    ) {
    new Button().Background(Colors.DarkRed)
        .FillHorizontal()
        .FillVertical()
        .Cell(column:0, row: 1),
    new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical()
        .Cell(column:1, row: 1),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal()
        .Cell(column:2, row: 1),
}.Background(Color.FromArgb("#1d1d1d"));
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/031m8702ewkncsufknyo.png)

The `Grid` is similar to `ZStack` in that the z-index (or depth) of the views are determined by their order in the body. 

Of course, you can span views across columns and rows as well. Here I'll add another `Button` to the end of the `Grid` and span it across three columns:

```csharp
new Button().Background(Colors.DarkGreen)
        .FillHorizontal()
        .FillVertical()
        .Cell(column:0, row: 0, colSpan:3),
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fcq3pdwmsjpg2zg6dwu3.png)


Now for some power-ups! James Clancey recently added `HGrid` and `VGrid` as well as a few helpers. The concept here is that you want to just provide a set of child views and have them displayed uniformly. To do this you can provide the number of cross-axis cells to render, and just add views. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ny8kqif09ru3j8u9mkre.png)

```csharp
new VGrid(5) {
    new Button().Background(Colors.DarkRed)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
    new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
        new Button().Background(Colors.LightSlateGray)
        .FillHorizontal()
        .FillVertical(),
    new Button().Background(Colors.DarkRed)
        .FillVertical()
        .FillHorizontal(),
}.Background(Color.FromArgb("#1d1d1d"));
```

Oof, despite not needing to declare the size of columns and rows, and not needing to specify the Cell for each view, that's a long block of repetitive code. 😖 Let me make that shorter while adding more content.

```csharp
new VGrid(5) {
    Enumerable.Range(0,20).Select(x=>
            new Button().Background((x % 2 == 0) ? Colors.DarkRed : Colors.LightSlateGray)
                .FillHorizontal()
                .FillVertical()
    ),
}.Background(Color.FromArgb("#1d1d1d"));
```

Voilà! You can do the same with `Grid`, and take advantage of some helpers like `NextColumn()` (it bumps the view to the next column).

## Basic UI Controls

Whether you're building a CRUD app or something more creative, you need a basic set of views to take user input and enable interaction. Comet ships today with:

| | | | |
| :-- | :-- | :-- | :-- |
| ActivityIndicator | Button | CheckBox | DatePicker | 
| Image | IndicatorView | ListView | ProgressBar | 
| RadioButton | Section | SectionedListView | SecureField | 
| ShapeView | Slider | Stepper | TabView | 
| Text | TextEditor | TextField | Toggle |

Within a `ShapeView` you can draw [shapes](https://github.com/dotnet/Comet/tree/dev/src/Comet/Shapes):  Capsule, Circle, Ellipse, Path, Pill, Rectangle, and RoundedRectangle.

Views support features such as shape clipping, setting borders (stroke), and shadows. 

For colors you have your choice of solid paint, linear and radial gradient paint, and more provided by [Microsoft.Maui.Graphics](https://github.com/dotnet/Microsoft.Maui.Graphics).

{% github dotnet/Microsoft.Maui.Graphics no-readme %}

You've likely picked up on this from the previous examples, as I already know you're an astute developer since you are here with me looking at Comet: to begin customizing a view, Comet provides a fluent syntax that builds upon the base established in the constructor. Chain together your customizations like this:

```csharp
new Text()
	.Color(Colors.White)
	.FontSize(64)
	.LineBreakMode(LineBreakMode.WordWrap)
	.HorizontalTextAlignment(TextAlignment.Center)
	.Margin(left:30,right:30),
new Button("Increment")
	.Frame(height:76)
	.FontSize(32)
	.Color(Colors.White)
	.Background(Colors.OrangeRed)
	.RoundedBorder(20)
	.Margin(left:30,right:30),
```

In this example the `Text` has a foreground color of white, a size of 64, enables word wrapping, centers the text, and spaces the view from the edges of the parent view. I don't need to "read" the `Button` code to you, however I would like to call your attention to `Frame`. This gives a view an explicit size, which on most any platform is a great idea to provide if you know the size in order to optimize layout performance.

{% github davidortinau/FancyCounter no-readme %}

## Random Tips

1. While hot reloading put all your code in a single file to reduce the need to restart the app.

2. You can comment out a single line in the chain while editing without breaking the app.

3. Provide size in `Frame` if you know it to improve layout performance.

4. Compose views into reusable functions [like my `HangulText` view](https://github.com/davidortinau/HangulClock/blob/main/Comet/src/HangulClock/MainPage.cs#L29-L40) in order to create a template for a common style of control.

5. Have fun and share your experiences on Twitter (I'm [@davidortinau](https://twitter.com/davidortinau)), GitHub, or with your 🦔.