Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Proposal: Make Grid Better #54

Open
micahl opened this issue Dec 8, 2018 · 44 comments
Open

Feature Proposal: Make Grid Better #54

micahl opened this issue Dec 8, 2018 · 44 comments
Labels
feature proposal New feature proposal team-Controls Issue for the Controls team

Comments

@micahl
Copy link
Contributor

micahl commented Dec 8, 2018

Proposal: Grid - More gain, less pain

Summary

Make a version of Grid that is easier to use and manipulate with capabilities that are on par with the CSS Grid.

Rationale

XAML's Grid is the most widely used Panel and is very versatile. However, it's also one of the more complicated panels and can be cumbersome. It doesn't lend itself well to easily positioning elements or simple adjustments (e.g. inserting a new row/column). Enhancing the Grid in a few simple ways will go a long way towards easing a developer's job.

Functional Requirements

# Feature Priority
1 Able to refer to rows / columns by either number or a name Must
2 Able to define spans as relative numbers or using names Must
3 Able to assign a friendly name to a region of the grid and associate child element's with that region by name versus the row(span)/col(span) #'s Could
4 Able to more succinctly define rows / columns, with options to be more explicit Should
5 Support auto flow where some items may be explicitly assigned and the rest implicitly fill in by tree order (with control over how it auto fills) Should
6 Support for defining auto-generated rows/columns that are implicitly created to position items with otherwise out-of-bounds grid indices Should
7 Support alignment options for a general policy that applies to all the Grid's children (individual elements can override) Could
8 Support a ShowGridLines property to aid in visual debugging the layout Could

Usage Examples

Named Row / Column

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="1*"/>
    <RowDefinition Name="bottomrow" Height="Auto"/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="1*"/>
    <ColumnDefinition Name="rightcol" Width="Auto"/>
  </Grid.ColumnDefinitions>

  <Button Grid.Row="bottomrow" Grid.Column="rightcol"/>

</Grid>

A More Succinct Syntax

<Grid RowDefinitions="Auto, 1*, {Name=bottomrow Width=Auto}"
      ColumnDefinitions="Auto, 1*, {Name=rightcol Width=Auto}">

  <Button Grid.Row="bottomrow" Grid.Column="rightcol"/>

</Grid>

Defining Named Areas
Specify a single Row (or Column) using just the name or the index. You can optionally provide a span by name or relative count with the comma syntax "fromRow,toRow" or "fromRow,#".

<Grid ColumnDefinitions="{Name=leftcol Width=Auto}, 1*, {Name=rightcol Width=Auto}"
      RowDefinitions="{Name=toprow Width=Auto}, {Name=body Width=1*}, {Name=bottomrow Width=Auto}">
      <Grid.AreaDefinitions>
        <AreaDefinition Name="imageArea" Row="toprow,bottomrow" Column="0"/>
        <AreaDefinition Name="header" Row="toprow" Column="leftcol,rightcol"/>
        <AreaDefinition Name="footer" Row="bottomrow" Column="leftcol,rightcol"/>
      <Grid.AreaDefinitions>

      <Image Grid.Area="imageArea"/>

      <TextBlock Grid.Area="Header"/>

      <Button Grid.Row="bottomrow" Grid.Column="rightcol"/>
</Grid>

More Succinctly Defined Areas

<Grid ColumnDefinitions="{Name=leftcol Width=Auto}, 1*, {Name=rightcol Width=Auto}"
      RowDefinitions="{Name=toprow Width=Auto}, {Name=body Width=1*}, {Name=bottomrow Width=Auto}"
      AreaDefinitions="{Name=imageArea Row=toprow,bottomrow Column=0},
             {Name=header Row=toprow Column=leftcol,rightcol},
             {Name=footer Row=bottomrow Column=leftcol,rightcol}">

      <Image Grid.Area="imageArea"/>

      <TextBlock Grid.Area="Header"/>

      <Button Grid.Row="bottomrow" Grid.Column="rightcol"/>
</Grid>

Auto-flow Layout w/ Auto-generated Rows (or Columns)

<Grid ColumnDefinitions="240, 1*"
      ColumnSpacing="18"
      AutoFlow="Row"
      AutoRowDefinitions="{MinHeight=80 Height=Auto}">
  <Grid.Resources>
      <Style TargetType="TextBlock">
          <Setter Property="HorizontalAlignment" Value="Right"/>
      </Style>
  </Grid.Resources>

  <TextBlock Text="Id"/>
  <TextBox x:Name="IdField"/>

  <TextBlock Text="Name"/>
  <TextBox x:Name="NameField"/>

  <TextBlock Text="Address"/>
  <TextBox x:Name="AddressField"/>

  <!-- Inserting a new label + field or re-ordering them doesn't require 
       updating all the Column/Row assignments -->

</Grid>

The AutoFlow property determines how elements that aren't explicitly assigned to an area / row / column will be placed. The default is 'None'. A value of 'Row' causes the layout to attempt to sequentially fill the columns of a row before moving to the next row. It can leave gaps by not considering if elements seen later could have fit on earlier rows.
A value of 'RowDense', however, would attempt to fill in the earlier gaps with elements seen later. RowDense can affect the visual ordering and could adversely impact accessibility. Similar behavior applies to values of Column and ColumnDense.

Detailed Feature Design

Open Questions

Would a Grid with these capabilities (i.e. named areas and autoflow + autogenerated rows/columns) simplify the way that you currently use Grid? For what scenario?
Which of the proposed enhancements would you find most useful?

  • compact syntax for defining rows/columns,
  • support for defining and using named columns and rows,
  • autoflow and auto-generated rows/columns)
@micahl micahl added the feature proposal New feature proposal label Dec 8, 2018
@micahl micahl self-assigned this Dec 8, 2018
@chrisglein
Copy link
Member

I assume in this that Grid.AutoFill maps to CSS grid's grid-auto-flow? (I stumbled on the name change at first)

@mrlacey
Copy link
Contributor

mrlacey commented Dec 8, 2018

I'm curious about this part of the Defining Named Areas example:

...
<AreaDefinition Name="imageArea" Row="toprow,bottomrow" Column="0"/>
...
<Image Grid.Area="imageArea"/>
...

How would the image be displayed in an area that covers multiple rows? (or equivalent for columns?)

Non-sequential rows or columns in an AreaDefinition has the potential to be very confusing. I'm not sure what I expect to happen here and so predict that multiple people could have multiple differing expectations. I'd rather see an AreaDefinition support Row and RowSpan (Or Column equivalents) rather than multiple non-sequential rows.
If there's a good reason to support non-sequential rows (from CSS or elsewhere) I'm happy to be convinced otherwise.

@mrlacey
Copy link
Contributor

mrlacey commented Dec 8, 2018

+1 for being able to name rows and columns. I can see this being a great way to improve the readability of code.

@mrlacey
Copy link
Contributor

mrlacey commented Dec 8, 2018

To add clarity to the Auto-fill Layout w/ Autogenerated Rows (or Columns) example, is this equivalent ?

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="240" />
        <ColumnDefinition Width="1*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition MinHeight="80" Height="Auto" />
        <RowDefinition MinHeight="80" Height="Auto" />
        <RowDefinition MinHeight="80" Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="HorizontalAlignment" Value="Right"/>
        </Style>
    </Grid.Resources>

    <TextBlock Text="Id" Grid.Column="0" Grid.Row="0" />
    <TextBox x:Name="IdField" Grid.Column="1" Grid.Row="0" />

    <TextBlock Text="Name" Grid.Column="0" Grid.Row="1" />
    <TextBox x:Name="NameField" Grid.Column="1" Grid.Row="1" />

    <TextBlock Text="Address" Grid.Column="0" Grid.Row="2" />
    <TextBox x:Name="AddressField" Grid.Column="1" Grid.Row="2" />

</Grid>

@micahl
Copy link
Contributor Author

micahl commented Dec 8, 2018

@chrisglein Yes, the autofill would behave the same as the CSS Grid's auto-flow.

@mrlacey The intention with Row="toprow,bottomrow" was that it be interpreted as a range "from,to" rather than a set of non-contiguous rows/columns. I'll clarify that in the proposal. I considered using a colon as the delimiter like Excel, but went with comma for now.
Re: auto-fill w/ autogenerated rows, your example is exactly it. Thanks!

@mrlacey
Copy link
Contributor

mrlacey commented Dec 9, 2018

@mrlacey The intention with Row="toprow,bottomrow" was that it be interpreted as a range "from,to" rather than a set of non-contiguous rows/columns. I'll clarify that in the proposal. I considered using a colon as the delimiter like Excel, but went with comma for now.

A comma is used as a separator elsewhere in this proposal so a colon to indicate a range sounds better.

@lindexi
Copy link

lindexi commented Dec 10, 2018

Define the undefined behaviors

XAML's Grid is the most widely used Panel and is very versatile. However, there are some undefined behaviors.

I hope we can define undefined behaviors.


Reading Tips: All of the examples described in this article are not common usages for Grid. (Microsoft is a great company. It will never do strange things in the common situation.)

Star Unit on Infinite space

Copy and paste the code below and run to view the result:

<Canvas>
    <Grid Height="100">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>
        <Border Grid.Column="0" Background="CornflowerBlue" Width="150" />
        <Border Grid.Column="1" Background="Tomato" Width="150" />
        <Border Grid.Column="2" Background="Teal" Width="150" />
    </Grid>
</Canvas>

See Scenario1 in demo.

The 1st column is 100-pixel fixed-width. The 2nd column is *, and the 3rd one is 2*. Then what's the visible width of the 2nd Border and the 3rd Border?

image

Did you predicate the result? Although the 2nd and the 3rd column width proportion is 1:2, the final visible proportion is 1:1.

There are flaws here, because you may suspect that the 3rd column is already twice as much as the 2nd column, but the right side is blank and cannot be seen. So now, we remove the Canvas and use HorizontalAlignment="Right". The new code is shown below:

<Grid HorizontalAlignment="Right">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    <Border Grid.Column="0" Background="CornflowerBlue" Width="150" />
    <Border Grid.Column="1" Background="Tomato" Width="150" />
    <Border Grid.Column="2" Background="Teal" Width="150" />
</Grid>

See Scenario2 in demo.

After running, you will find that there is no white space on the far right, that is to say, the 2nd and 3rd columns do not have a 1:2 ratio - they are equal.

So where is the lost space? Let's resize the window to check it.

narrow the window

Even if there is space left on the left, the right side begins to clip the element space! Can we say that the length of a missing * length has gone to the left? Obviously not. However, we can guess that the clipping of the right side of the element begins at the 1:2 ratio.

Star Unit at the Size Just Required

HorizontalAlignment="True" helps us a lot to distinguish whether the right side really occupies space. So we continue the testing on the right-alignment.

Now, we modify the 2nd column Border to span the 2nd and 3rd columns. The 3rd column Border is placed into the 2nd column. (In other words, our 3rd column does not contain any Border.)

<Grid HorizontalAlignment="Right">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    <Border Grid.Column="0" Background="CornflowerBlue" Width="150" />
    <Border Grid.Column="1" Grid.ColumnSpan="2" Background="Tomato" Width="150" />
    <Border Grid.Column="1" Background="Teal" Width="150" />
</Grid>

See Scenario3 in demo.

The new behavior did not show much surprise to us because we have seen the behavior last section. The 3rd column disappeared, and the 2nd column still lost the 1:2 ratio.

Narrow the window again.

Narrow


the window


again


to


view


the behavior

Why did the tomato Border suddenly appear when the window was narrowing? Why is there a blank space on the right side of the tomato Border?

If we have realized in the last post section that the right-side space is lost when it is right-aligned, why does the white space appear suddenly in the right-side again?

I tried to slightly increase the width of the second Border. Suddenly, I reproduced the strange behavior that I reproduced just now when resizing the window!

The Proportion of Auto Size

Now, abandon the previous right-aligned test method and no longer use the * width to separate the Grid. We use Auto instead.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Border Width="159" Grid.ColumnSpan="3" HorizontalAlignment="Center" Background="PaleGreen" />
    <Border Width="28" HorizontalAlignment="Left" Background="#7FFF6347" />
    <Border Width="51" Grid.Column="1" HorizontalAlignment="Center" Background="#7FC71585" />
    <Border Width="28" Grid.Column="2" HorizontalAlignment="Right" Background="#7F008080" />
</Grid>

See Scenario4 in demo.

Specifically, we have four Border, placed in three columns of Auto size. The first Border spans three columns, and its size is longer than all the others, reaching 159. The remaining three Border each occupy a column, with two sides of equal length and a slightly longer middle.

How are the columns in the actual layout divided? Here is the column width that the designer shows for us:

Where do 46, 69, 46 come from? Could it be that the proportion of 46:69 is the same as that of 28:51? However, the actual calculation result is not!

What if this is a calculation error?

So let's look at the other two sets of values for the three Border: 50:50:50 and 25:50:25.


50:50:50


25:50:25

In 50:50:50, we eventually get the 1:1:1 proportion. But the ratio of column widths in 25:50:25 is far from 1:2:1. That is, in fact, the Grid does not calculate the column widths by the proportion to the size of the element.

The same Element Size but Different Column Width

In the experiment in the previous section, we notice that the same size brought about the same final visible size regardless of the proportion. However, this conclusion still can be subverted.

Now, we will replace 3 columns with 4 columns, and the number of Border will be replaced with 6.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Border Width="159" Grid.ColumnSpan="3" HorizontalAlignment="Center" Background="PaleGreen" />
    <Border Width="159" Grid.Column="1" Grid.ColumnSpan="3" HorizontalAlignment="Center" Background="PaleGreen" />
    <Border Width="28" HorizontalAlignment="Left" Background="#7FFF6347" />
    <Border Width="51" Grid.Column="1" HorizontalAlignment="Center" Background="#7FC71585" />
    <Border Width="51" Grid.Column="2" HorizontalAlignment="Center" Background="#7FC71585" />
    <Border Width="28" Grid.Column="3" HorizontalAlignment="Right" Background="#7F008080" />
</Grid>

See Scenario5 in demo.

Specifically, the first Border spans the first three columns, and the second Border spans the last three columns, the same length as the long Border of the previous section. The third and sixth Borders are on two sides and are as short as the previous Border. The middle two Borders are as long as the previous Border. Just like the picture that is shown below.

What is the width of the columns laid out at this time?


▲ 32:65:65:39

Wait! Where did the 39 come from? If the equal-size Border in the previous section would get equal-sized column widths, then this will also subvert! In fact, even if the proportion of the column width to the proportion of elements is the same at this time, there are as infinitely as many solutions under this layout. WPF picks only one out of this infinite number of solutions - and it cannot explain itself!

The conclusion of the Grid undefined behavior

In summary, the Grid layout has some unreasonable behaviors under special circumstances. I call them "the undefined behaviors". These undefined behaviors are summarized in the following three points:

  1. Infinite layout space with * unit size
  2. * unit column/row with multiple-span elements
  3. Auto size in all column/row

However, you may think that I use the Grid in incorrect ways. However, as an API that exposes the behaviors, the behavior itself is also a part of the API. It should have clear traceable and documentable behavior instead of being explored and guess and failed by the user.

Microsoft does not have any official documents that disclose these bizarre behaviors, and I have not found such behavior in any third-party references (this post is my own conclusion). I think that Microsoft did not publish this kind of documents because the behaviors are too bizarre to be documented!

I hope we can define this behaviors.

See The undefined behaviors of WPF Grid (the so-called bugs) - walterlv

@walterlv
Copy link

Agree to fix that undefined grid behaviors. This may not be bugs, but it's behavior is really undescriptable.

@chrisglein
Copy link
Member

@lindexi A clear advantage of WinUI living here on GitHub is that we can A) have the source code for the Grid layout readily available, B) use Issues to track either incorrect behaviors or the lack of documentation for edge case behaviors. It seems like together that can help shed light on undefined behaviors and get them to be defined?

@micahl framed this proposal as a desire to absorb some of the goodness from CSS's grid and make a more expressive Grid for XAML via WinUI. As interesting as the cases you've raised are, it seems like there's enough meat there to raise a separate discussion about these existing edge cases? I don't want to distract from the proposal @micahl laid out. Or at the very least phrase these edge cases in a way that are more relevant to the CSS comparison. For example, you can model that first scenario in CSS and talk about how the behavior is different between that and the XAML equivalent and how we feel about those differences:

<style>
.grid-container {
  display: grid;
  grid-template-columns: 100px 1fr 2fr;
  grid-auto-flow: column;
  height: 100px;
}
.item1 {
  background-color: cornflowerblue;
  width: 150px;
}
.item2 {
  background-color: tomato;
  width: 150px;
}
.item3 {
  background-color: teal;
  width: 150px;
}
</style>

<div class="grid-container">
  <div class="item1"></div>
  <div class="item2"></div>
  <div class="item3"></div>  
</div>

@dotMorten
Copy link
Contributor

IMHO a lot of the feature presented above to improve Grid, it really sounds like what you're looking for is RelativePanel.

I do agree ColumnDefinitions should have a simple syntax, and that could easily be achieved with a string converter. Ie <Grid ColumnDefinitions="1*,200,Auto,2*" />

@michael-hawker
Copy link
Collaborator

We also added an enhanced UniformGrid to the toolkit; so it'd be interesting to think about the overlap between those scenarios and the ones proposed here.

I think the main thing the UniformGrid adds is that it's also effectively an ItemsControl that can take a collection of items to layout, which seems to be a bit what the 'AutoFill' feature is trying to accomplish as well?

I guess the question is could these new Grid 'extensions' cover the UniformGrid scenarios as well?

@chrisglein
Copy link
Member

@michael-hawker It does seem like some of the AutoFill/AutoRows options could eliminate the need to use UniformGrid for many scenarios. I'm not sure if it covers all. So I walked through the scenarios listed in the document you linked and recreated them in CSS grid:

https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/uniformgrid#grid-properties

<style>
.grid-container {
  display: grid;
  grid-template-columns: 150px 150px 150px;
  grid-column-gap: 24px;
  grid-row-gap: 24px;
  grid-auto-rows: 150px;
  grid-auto-flow: row;
}
</style>

<div class="grid-container">
  <div style="background-color: aliceblue">1</div>
  <div style="background-color: cornsilk">2</div>
  <div style="background-color: darksalmon">3</div>  
  <div style="background-color: gainsboro">4</div>  
  <div style="background-color: lightblue">5</div>  
  <div style="background-color: mediumaquamarine">6</div>  
  <div style="background-color: mistyrose">7</div>  
</div>

https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/uniformgrid#sized-children

<style>
.grid-container {
  display: grid;
  grid-template-columns: 150px 150px 150px;
  grid-auto-rows: 150px;
  grid-auto-flow: row;
}
</style>

<div class="grid-container">
  <div style="background-color: aliceblue; grid-column-end: span 2;">1</div>
  <div style="background-color: cornsilk">2</div>
  <div style="background-color: darksalmon">3</div>  
  <div style="background-color: gainsboro; grid-row-end: span 2;">4</div>  
  <div style="background-color: lightblue">5</div>  
  <div style="background-color: mediumaquamarine">6</div>  
  <div style="background-color: mistyrose">7</div>  
</div>

https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/uniformgrid#fixed-child-locations

<style>
.grid-container {
  display: grid;
  grid-template-columns: 150px 150px 150px;
  grid-auto-rows: 150px;
  grid-auto-flow: row;
}
</style>

<div class="grid-container">
  <div style="background-color: aliceblue; grid-column-start: 2; grid-row-start: 2;">1</div>
  <div style="background-color: cornsilk">2</div>
  <div style="background-color: darksalmon">3</div>  
  <div style="background-color: gainsboro;">4</div>  
  <div style="background-color: lightblue">5</div>  
  <div style="background-color: mediumaquamarine">6</div>  
  <div style="background-color: mistyrose">7</div>  
</div>

https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/uniformgrid#override-rows-and-columns
This one I haven't been able to figure out yet. I feel like it should be possible.

https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/uniformgrid#orientation
I don't think this scenario is possible with CSS grid. Unlike CSS flex layout's flex-direction there doesn't seem to be a similar grid-direction. This is something we could include if there was need for it.

Note that for many of the above examples I'm making a hard assumption about 3 columns instead of re-flowing. CSS grid seems pretty adverse to re-flowing the layout in that way, and it doesn't handle constraints in the way I expect. Again this is something that CSS flex layout does better with flex-wrap. We'd want a XAML Grid's auto flow options to be able to handle wrapping.

If you have any other UniformGrid scenarios please share them. It does feel like we can achieve a superset Grid layout.

@YuliKl
Copy link

YuliKl commented Dec 11, 2018

There's a request on UserVoice to ShowGridLines that ought to be considered for this feature proposal.

@micahl
Copy link
Contributor Author

micahl commented Dec 12, 2018

@chrisglein Isn't the Orientation behavior covered by the grid-auto-flow in CSS? An orientation of Horizontal is the same as auto-flow of row and Vertical is column?
The CSS Grid's auto-flow also supports a row-dense and column-dense option. Thinking about whether these enhancements might supplant the need for other, similar panels such as UniformGrid... If there was a row-dense/column-dense option on Grid then I believe it would replace our existing VariableSizedWrapGrid.

@chrisglein
Copy link
Member

@chrisglein Isn't the Orientation behavior covered by the grid-auto-flow in CSS? An orientation of Horizontal is the same as auto-flow of row and Vertical is column?

@micahl I was thinking of the comparison to flex-direction=row-reverse/column-reverse. CSS flex gives you the ability to reverse the order where CSS grid does not.

If I'm understanding "dense" correctly it's really about sacrificing element order so that the grid can be more tightly packed. Consider the difference between "row" and "row dense" in this example:

<style>
.grid-container {
  display: grid;
  grid-template-columns: 150px 150px 150px;
  grid-auto-rows: 150px;
  grid-auto-flow: row dense;
}
</style>

<div class="grid-container">
  <div style="background-color: aliceblue">1</div>
  <div style="background-color: cornsilk">2</div>
  <div style="background-color: darksalmon; grid-column: span 2">3</div>  
  <div style="background-color: gainsboro;">4</div>  
  <div style="background-color: lightblue">5</div>  
  <div style="background-color: mediumaquamarine">6</div>  
  <div style="background-color: mistyrose">7</div>  
</div>

The thing that makes that example work is having an item with a span that causes it to not fit. This mode would actually make sense in a wrapping flex layout as well (looking forward for smaller items before wrapping a larger item to the next row), but to my knowledge that doesn't exist.

Which isn't what I thought "dense" was going to be at first glance (although the behavior does make sense to me). I was at first looking for a knob to control whether undesignated cells filled in at the earliest open spot or after the last filled spot. Do you fill in the middle or append at the end? Which is probably a useful option although isn't what "dense" does.

@micahl
Copy link
Contributor Author

micahl commented Dec 12, 2018

Right. The downside to the dense behavior is that it might adversely impact an accessibility experience because of how it might jumble around the ordering.
Another observation... the auto flow behavior might be useful in a forms layout scenario where some items are explicitly assigned and the others flow around it. The non-dense behavior would allow someone to explicitly nudge an item over to an assigned column and intentionally leave some empty space because subsequent items would be placed after it rather than potentially filling in gaps.

Updates to the proposal...

  • @chrisglein Renamed AutoFill to AutoFlow given the CSS precedent
  • @YuliKl Added ShowGridLines to the requirements. Thanks!

@mrlacey I agree that the comma for specifying a span can be confusing given how its used elsewhere. For the sake of discussion some alternatives might be:

  • A colon ':' For example, Column="leftcol:rightcol"
  • A double-dot '..' For example, Column="leftcol..rightcol".
  • Mimic the CSS syntax with the slash '/' For example, Column="leftcol / rightcol"
  • Don't do anything special. Instead have RowSpan and ColumnSpan properties on AreaDefinition that accept a number or name.

From the perspective of readability the colon seems to blend into the text when using names as opposed to numbers which makes it less decipherable. A double-dot '..' fairs better IMO. The slash just makes me think of division. The explicit RowSpan and ColumnSpan is familiar, albeit verbose.

I'm interested to hear others opinions.

@michael-hawker
Copy link
Collaborator

@micahl I like .. for the slice syntax, though it'd be good to track what C# is doing for this as well, as outlined here: dotnet/csharplang#185

ShowGridLines would be great as WPF had it as well, so another gap filled. I'd use it a lot for debugging and in some other projects.

@michael-hawker
Copy link
Collaborator

FYI, to call out, a similar discussion was opened on the WPF Issues: dotnet/wpf#166

@thomasclaudiushuber
Copy link
Contributor

thomasclaudiushuber commented Dec 14, 2018

@michael-hawker Yes, I linked it already, as you can see above. Thanks for explicitly mentioned it here.

@micahl Thanks for the proposal. Here some thoughts:

  • We shouldn't rename the properties RowDefinitions and ColumnDefinitions to Rows and Columns. Instead I think we should keep the original properties and implement a TypeConverter to get the short hand syntax. That stuff is already discussed here Grid improvements dotnet/wpf#166 and it's the main point of the WPF issue.
  • I like the idea of areas. But I think it's much harder to read than explicit columns and rows on an element. In a large document I look at an element, I see the area. Then I scroll up to the area definition, then I look at its rows and columns and then I look at the rows and columns themselves to finally understand how the element is layed out. While I see the value of such an area and while I like it, I think it also overcomplicates things a bit, as you get more ways to do the same thing
  • Naming RowDefinitions and ColumnDefinitions sounds like an interesting idea. But when it comes to RowSpan or ColumnSpan it gets more complex, as it requires a start and an end row/column instead of a simple number. I guess if the tooling would have support for numbers, we don't need names. For example Visual Studio could suggest this quick tip when I place the cursor behind a RowDefinition:
    image
    Then that could automatically adjust all the numbers accordingly and the problem is solved. We could think about the UX a bit more, it's just an initial idea.

On the other points I agree with @dotMorten. The RelativePanel has a lot of that stuff mentioned. I think the Grid should be improved, but it should also stay straight and simple. From my point of view, we should start with the simpler syntax for RowDefinitions and ColumnDefinitions by implementing a Type Converter and we should improve the tooling.

@lindexi
Copy link

lindexi commented Dec 14, 2018

@thomasclaudiushuber I like the idea of areas and binding the name. I always modify the Grid's row and column when we change the UI and I need change all the element's row and column in the Grid when I insert the row or the column at the first row or the first column. If I can bind the name that I can add the row or the column without modifying the element.

@micahl
Copy link
Contributor Author

micahl commented Dec 14, 2018

@thomasclaudiushuber I've updated the proposal to follow the current naming precedence with RowDefinitions and ColumnDefinitions (and AreaDefinitions for consistency).

I agree there's ways that tooling can help in making it easier to use Grid (and other layouts) and would hope that any improvements to Grid (UWP and WPF) are complementary to any future improvements in the tooling. @diverdan92 as fyi

@trodent83
Copy link

trodent83 commented Dec 17, 2018

@micahl
I think it would also help a lot if we would have a Kind of mix of grid and uniform grid functionality where we could specify a general theme for the grids, and then redefine them with explicit overwrides.

So something similar to this:

<Grid Rows="5" Columns="6" RowHeight="Auto"> <Grid.RowDefinitions> <RowDefinition Row ="2" Height="*"/> </Grid.RowDefinitions> </Grid>
because in most cases we have basicly UniformGrid which only differ in one or two rows and columns.

@micahl
Copy link
Contributor Author

micahl commented Dec 18, 2018

Thanks, @trodent83. Based on your comment I took a moment to look across a few different apps using Grid to see what common patterns existed for rows/column definitions. Granted this wasn't a very rigorous data collection exercise, but my observations were that:

  1. Auto-sizing was used more than *-sizing
  2. The row (or column) definitions for a Grid were often homogeneous (e.g. all Auto or all *) with a few that differed (similar to your assertion)
  3. Most Grids I saw only had 2-3 row (or column) definitions. At least in my sample I didn't see anything with more than 8.

Assuming what I observed holds more generally and there are usually only a handful of column/row definitions (less than 3) I'd suspect that a common approach would be to just rely on the shortcut syntax for Row/ColumnDefinitions. For example:
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*"/>

@lindexi
Copy link

lindexi commented Dec 19, 2018

Add RowSpanDefinitions to Grid. As RowSpanDefinitions name says this property can define the row span.

How to use?

                <Grid Name="Root" RowDefinitions="Auto,Auto,Auto,Auto,Auto"
                      ColumnDefinitions="Auto,Auto,Auto,Auto,Auto"
                      RowSpanDefinitions="{Name=Foo Start=1 End=2},{Name=TheNext Start=1 End=3}">
                          <Rectangle Name="Back" Grid.Column="1" Grid.RowSpan="Foo"/>
              </Grid>

It like the Area by @micahl but we can use it in different column. And we can think of it is an area that has not to define where is it column.

image

We define another set of attributes ColumnSpanDefinitions in pairs.

image

But when we use Grid.RowSpan to bind the RowSpanDefinitions, can we use Grid.Row property?

@trodent83
Copy link

@micahl
It is true that in most cases the Grids only have a few rows and columns, and the simplification of the Definition in these cases would help a lot, but sadly sometimes this is not the case, and in these situations the simplified Definition would not only not help much, but quite the opposite as it would make the code less readable.

And I thing the two aproaches could be merged relatively easy, and so we could have the best of the two worlds.

@micahl
Copy link
Contributor Author

micahl commented Dec 22, 2018

Thanks for the suggestions! At this point in the conversation I think we need to start considering what the merge would look like if the proposed options are combined and ask ourselves, "Is the behavior for various combinations still reasonable? Does it create conflicts or duplication?" The tl:dr; answer to those is No and Yes. Let's see if we can whittle things down to make it Yes and No.

@lindexi I believe AreaDefinitions can provide the same functionality as RowSpanDefinitions and ColumnSpanDefinitions. It seems reasonable to set a combination of Grid.Row/RowSpan/Column/ColumnSpan properties on an element in addition to specifying a Grid.Area. But, what would it mean and what takes precedence? I believe having the Row(Span)/Column(Span) values take precedence would make the most sense. They'd behave like overrides on the area. For example:

<Grid Name="Root"
      Rows="5" RowHeight="Auto"
      Columns="5" ColumnWidth="Auto"
      AreaDefinitions="{Name=Foo Row=1..2}, {Name=TheNext Row=1..3}">
    <Rectangle Name="Back" Grid.Column="1" Grid.Area="Foo"/>
</Grid>

Since the definition of the Foo area didn't explicitly set a Column it would have to use a default (i.e. zero). Explicitly setting the Grid.Column="1" in the example will mean that the Rectangle will be on row 1 spanning to row 2 and end up in column 1 (instead of the default column 0).

Taking this exploration a bit further to consider the combinations of other properties... What would happen in the contrived example below?

<Grid Name="Root"
      Rows="3"
      RowHeight="Auto"
      RowDefinitions="{Name=BackRow Row=1}"
      Columns="3"
      ColumnWidth="Auto"
      AutoFlow="Row"
      AutoRowDefinitions="40"
      AutoColumnDefinitions="60"
      AreaDefinitions="{Name=BackArea1 Column=1 Row=1..3},{Name=BackArea2 Column=1 Row=BackRow},{Name=BackArea3 Column=1 Row=BackRow..6">

    <Rectangle x:Name="Rect1" Grid.Column="1" Grid.Row="BackRow"/>

    <Rectangle x:Name="Rect2" Grid.Column="1" Grid.Row="1..2"/>

    <Rectangle x:Name="Rect3" Grid.Area="BackArea1"/>

    <Rectangle x:Name="Rect4" Grid.Area="BackArea2"/>

    <Rectangle x:Name="Rect5" Grid.Area="BackArea3"/>

    <Rectangle x:Name="Rect6" Grid.Column="4" Grid.Area="BackArea1" Grid.Row="4"/>

    <Rectangle x:Name="Rect7" Grid.Column="4" Grid.Area="BackArea3" Grid.Row="4"/>
</Grid>

My initial reaction is that it can become confusing very quickly. I believe it would be something equivalent to the markup below. Some issues raised by this exercise are:

  • How should a RowHeight/ColumnWidth property behave with the AutoRowDefinitions/AutoColumnDefinitions?
    • Having both is confusing and it seems like they're close. The shorter name better aligns with the general goal of a more succinct syntax so... drop AutoRow/ColumnDefinitions in favor of RowHeight and ColumnWidth? IMO, yes.
  • Would named Areas be that useful in practice? Or do they just make things more complicated for someone to mentally parse what's going on in the markup?
    • They definitely increase the potential for edge cases when combined with other properties. And, as was pointed out earlier, areas just give more ways to do the same thing which suggests its duplicate functionality. Grid can be difficult to understand at times. Combining AreaDefinitions with the other properties seems to make that even more challenging and not a 'Must' have.
<Grid Name="Root">
    <Grid.RowDefinitions>
        <!-- These rows come from the Rows="3" RowHeight="Auto" properties. -->
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <!-- These are autogenerated to provide the filler rows until the actual row referenced by BackArea3. -->
        <RowDefinition Height="40"/>
        <RowDefinition Height="40"/>

        <!-- This is created so that the row referenced by Rect6 and 7 exists. 
             It's Auto by default to mimic what the CSS Grid seems to do. Should it be 
             based on the RowHeight property instead?  How does RowHeight behave with 
             AutoRowDefinitions? -->
        <RowDefinition Height="Auto"/>

        <!-- These are autogenerated to provide the filler rows until the next row -->
        <RowDefinition Height="40"/>
        <RowDefinition Height="40"/>

        <!-- This is row is required to satisfy the rowspan that comes from combining 
             Rect7's Grid.Row with the BackArea3 area definition.  The row's height is
             Auto by default to mimic what the CSS Grid seems to do. Same question as 
             before. Should it be based on the RowHeight property instead? -->
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <!-- the first 3 Auto columns come from the Columns="3" ColumnWidth="Auto" properties -->
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
        <!-- the next 2 columns are autogenerated to create the fifth column (Column="4") that Rect6 and Rect7 assume to exist -->
        <ColumnDefinition Width="60"/>
        <ColumnDefinition Width="60"/>
    </Grid.ColumnDefinitions>

    <Rectangle x:Name="Rect1" Grid.Column="1" Grid.Row="1" Grid.RowSpan="2"/>

    <!-- Row=1..2 meant row 1 to row 2.  It becomes row 1 with a span of 2. -->
    <Rectangle x:Name="Rect2" Grid.Column="1" Grid.Row="1" Grid.RowSpan="2"/>

    <Rectangle x:Name="Rect3" Grid.Column="1" Grid.Row="1" Grid.RowSpan="2"/>

    <Rectangle x:Name="Rect4" Grid.Column="1" Grid.Row="1" Grid.RowSpan="2"/>

    <!-- Row=1..6 meant from row 1 to row 6. It becomes row 1 with a span of 5. -->
    <Rectangle x:Name="Rect5" Grid.Column="1" Grid.Row="1" Grid.RowSpan="5"/>

    <!-- BackArea1 had Row=1..3 which would be row 1 with a span of 2, but the 
         explicit Grid.Row="4" overrides the starting position (only). -->
    <Rectangle x:Name="Rect6" Grid.Column="4" Grid.Row="4" Grid.RowSpan="2"/>

    <!-- BackArea3 had Row=Back..6 (same as 1..6) which would be row 1 with a span of 5,
         but the explicit Grid.Row="4" overrides the starting position (only) -->
    <Rectangle x:Name="Rect7" Grid.Column="4" Grid.Row="4" Grid.RowSpan="5"/>

</Grid>

If you made it through that and it made any sense then hats off to you. ;)

@lindexi
Copy link

lindexi commented Jan 12, 2019

@micahl This is my lack of consideration. I find all of my codes and I can not find any app use it.

@JustinXinLiu
Copy link

Other ideas?

I came across this post talking about subgrid in CSS and immediately remembered I had to deal with similar cases in the past and the solution was either to use a fixed width, or manually re-calculate it whenever a new item is added in.

I don't think this is a very common scenario but could be something to have a think about. ;)

@mdtauk
Copy link
Contributor

mdtauk commented Jan 12, 2019

Uncommon scenarios may be uncommon, because they are too difficult to implement with current controls and layout engines.

@micahl
Copy link
Contributor Author

micahl commented Jan 12, 2019

Good scenario. Reading the post it reminded me of the shared sizing capability that WPF supports on its Grid which helps address that exact kind of situation.

Another approach (that honestly warrants its own separate proposal for discussion) is to introduce the concept of attached layout more broadly than repeater controls. Separating the pure layout logic from the container (e.g. Panel) would allow a given layout / sizing to be shared across separate containers.
For example, if the layout logic for Grid was also available as a GridLayout type that could be attached to a container (e.g. LayoutPanel) then it would be possible to do something like the below. Despite not being part of the same container the TextBlocks could be visually aligned:

<Page.Resources>
    <GridLayout x:Key="sharedGridLayout" AutoFlow="Row" AutoColumnDefinitions="48,Auto"/>
</Page.Resources>

<LayoutPanel Layout="{StaticResource sharedGridLayout}">
    <FontIcon/>
    <TextBlock Text="Lorem"/>
    <FontIcon/>
    <TextBlock Text="Lorem ipsum dolor"/>
</LayoutPanel>

<!-- ... -->

<LayoutPanel Layout="{StaticResource sharedGridLayout}">
    <FontIcon/>
    <TextBlock Text="Lorem ipsum ..."/>
    <FontIcon/>
    <TextBlock Text="Lorem"/>
</LayoutPanel>

@JustinXinLiu
Copy link

Amazing. I totally forgot about IsSharedSizeScope in WPF.

The other approach works even better IMO given that's mostly where we use repeated layouts.

@JustinXinLiu
Copy link

Just thought of something else. ;)

Have we considered making RowDefinitions, ColumnDefinitions and AreaDefinitions as attached properties that are data-binding friendly? Imagine this scenario:

<Style TargetType="MyControl">
    <Setter Property="Grid.RowDefinitions" Value="{Name=toprow Height=Auto}, {Name=body Height=1*}, {Name=bottomrow Height=Auto}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="MyControl">
                <Grid RowDefinitions="{TemplateBinding Grid.RowDefinitions}">
                    ...
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<MyControl Grid.RowDefinitions="{Name=toprow Height=1*}, {Name=body Height=4*}, {Name=bottomrow Height=Auto}" />

Currently, I will have to duplicate and modify the entire style if I want to change the layout of inner elements of a control. If they were bindable, I could simply override RowDefinitions on the control itself.

@mdtauk
Copy link
Contributor

mdtauk commented Jan 16, 2019

We have gone from XAML providing best in class Grid panels, with CSS and HTML utterly failing. To HTML CSS having a very flexible solution as well as powerful.

When final decisions are made to pursue a new Grid control, I think it should be done as a new control at least until it can reach the maturity of the current Grid control.

So far what @JustinXinLiu has been considering, are tweaks and simpler XAML notation for the Grid. But if this new enhanced Grid was done as a new control, more options could be considered as well as breaking changes as the expectation wont be just simply substituting one for another.

Things like:

  • Gutters / Column and Row gaps, with or without border lines;
  • Per grid cell borders, padding, backgrounds;
  • Row and Column spans/merges;
  • Responsive re-flowing of cells/content;
  • Area and region naming/styling;
  • Dynamic placement of items in grid areas;
  • Insertion of repeated or data-bound rows and columns with Child(X) or some other means of specifying where new rows and columns are placed;

@bschoepke
Copy link

@micahl @jevansaks CSS Grid properties are now animatable in Firefox Nightly:
https://twitter.com/vlh/status/1085181353283543041

I know XAML layout currently runs on the UI thread but perhaps this new Grid provides an opportunity to make fast layout transforms like this easier--even if some smoke and mirrors are required, like automatically animated clips.

@JustinXinLiu
Copy link

@bschoepke nice one, but I am not sure if it's a Grid specific thing. How about fluid layout for all panel controls?

P.S. Currently we can use Visual and implicit animations to achieve similar background animation. Like you said, animating its contents will need composition clip.

@michael-hawker
Copy link
Collaborator

Linking issue from the toolkit, closed ours out for now as it's an open item here. For reference, here's the WPF Property for ShowGridLines for back-compat. And the UserVoice item currently has 18 votes for this property.

@mdtauk
Copy link
Contributor

mdtauk commented Jan 22, 2019

@bschoepke nice one, but I am not sure if it's a Grid specific thing. How about fluid layout for all panel controls?

P.S. Currently we can use Visual and implicit animations to achieve similar background animation. Like you said, animating its contents will need composition clip.

What about making this enhanced Grid a CompositionGrid which moves the layout from UI Thread to the Composition Layer, and becomes flexible in how and where items will be layed out, and allows for flexible reflow as the size changes, whilst keeping it smooth, and enabling future advances like lifting elements out for Mixed Reality 3D presentation.

@micahl
Copy link
Contributor Author

micahl commented Mar 21, 2019

Hey folks, I only recently returned from being gone since mid-January and have been catching up on things. I apologize if it seemed like I went dark. If it happens again I'll be sure to mention it beforehand or at least set my GitHub status.

Good discussion and thanks for all the input!

@JustinXinLiu, its interesting to think about lifting the layout out of a control such that it can be more easily styled without requiring you to re-template. For example, if a control's template parts were assigned their slot in the layout by name (i.e. Grid.Area) and if it was possible to set the Grid definitions the control should use then I believe it would make changing a control's internal layout <Style>-able.

@mdtauk, moving all of layout from the UI thread to the composition layer would be pretty gnarly (in ways both good and not so good). Depending on the desired UX there might be ways to achieve it without moving everything off the UI thread.

@dotMorten
Copy link
Contributor

This proposal is rather big (albeit great), I've created a separate proposal for just a simpler string-conversion based syntax of Row and Column definitions (linked above here) that might be a good simple first-step that is very achievable to do and doesn't require a lot of new API or behavior.

@nandin-borjigin
Copy link

nandin-borjigin commented Feb 4, 2022

I've started a dicussion in WindowsCommunityToolkit to support dynamic layout switching in Grid, with a working PR. The PR also introduces the concept of AreaDefinition and I borrowed(stole) the idea from CSS grid-template-areas. I personally think the approach is succinct and intuitive while I'm open to any suggestion. It would be great if we can merge these two proposals and finally produce a more powerful Grid.

I used plain string to describe AreaDefinition. The element names are listed in row-major order, separated by white space, and semicolons are used to separate different rows.

An example layout like this

    +----------+----------+----------+
    |          | ElementB | ElementC |
    | ElementA +----------+----------+
    |          |       ElementD      |
    +----------+---------------------+
    |             ElementE           |
    +--------------------------------+

could be expressed simply like below:

ElementA ElementB ElementC;
ElementA ElementD ElementD;
ElementE ElementE ElementE

Complete example

The example defines two possible layouts for a grid and supports switching them dynamically (by setting the GridExtensions.ActiveLayout property. The part that is related to this discussion is the way that I used to describe an AreaDefinition.

<Grid x:Name="RootGrid" GirdExentions.ActiveLayout="Normal">
    <!-- Declaratively define the possible layouts. -->
    <!-- GridExtensions.Layouts is a dictionary of GridLayoutDefinition -->
    <GridExtensions.Layouts>
        <GridLayoutDefinition x:Key="Normal">
            <!-- A GridLayoutDefinition consists of -->
            <!-- row definitions, column definitions and an area definition -->
            <GridLayoutDefinition.RowDefinitions>
                <RowDefinition Height="Auto"/>
            </GridLayoutDefinition.RowDefinitions>
            <GridLayoutDefinition.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </GridLayoutDefinition.ColumnDefinitions>
            <!-- Area definition just simply puts down -->
            <!-- children names in desired order -->
            Number Title Description
        </GridLayoutDefinition>
        <GridLayoutDefinition x:Key="Narrow">
            <GridLayoutDefinition.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </GridLayoutDefinition.RowDefinitions>
            <GridLayoutDefinition.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </GridLayoutDefinition.ColumnDefinitions>
            Number      Title; <!-- semicolon is used to separate differnt rows -->
            Description Description <!-- row/column span is expressed by repeating the elment name -->
        </GridLayoutDefinition>
    </GridExtensionstensions.Layouts>
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup>
            <VisualState x:Name="NarrowState">
                <VisualState.StateTriggers .../> <!-- Trigger implementation omitted -->
                <VisualState.Setters>
                    <!-- Only ActiveLayout property on the root grid needs to change according to the visual state -->
                    <Setter Target="RootGrid.(GridExtensions.ActiveLayout)" Value="Narrow">
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <TextBlock x:Name="Number">1</TextBlock>
    <TextBlock x:Name="Title">Lorem Ipsum</TextBlock>
    <TextBlock x:Name="Description">Lorem ipsum dolor sit amet...</TextBlock>
</Grid>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature proposal New feature proposal team-Controls Issue for the Controls team
Projects
None yet
Development

No branches or pull requests