Blog

posts by Fons Sonnemans

XAML AppBarElementContainer

0 Comments
By Fons Sonnemans, 21-aug-2018

I have been playing with the new Windows Insider Preview SDK build 17733. It contains a new control named AppBarElementContainer. This control allows you to add other controls then the AppBarButton, AppBarSeparator and AppBarToggleButton to a CommandBar or the "depricated" AppBar.

Demo

AppBarElementContainer is a container control so you can place any control (which fit inside the limited space) into it. In the next example I used for this demo a ComboBox and a Slider.

<Page x:Class="App1.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App1"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <CommandBar VerticalAlignment="Top">
            <CommandBar.PrimaryCommands>
                <AppBarButton Icon="Save"
                              Label="Save" />
                <AppBarElementContainer>
                    <ComboBox Width="200"
                              Margin="0,4,0,0"
                              SelectedIndex="0">
                        <ComboBoxItem Content="A" />
                        <ComboBoxItem Content="B" />
                        <ComboBoxItem Content="C" />
                    </ComboBox>
                </AppBarElementContainer>
                <AppBarElementContainer>
                    <Slider Width="200"
                            Margin="4" />
                </AppBarElementContainer>
                <AppBarButton Icon="Delete"
                              Label="Delete"
                              LabelPosition="Collapsed" />
            </CommandBar.PrimaryCommands>
            <CommandBar.SecondaryCommands>
                <AppBarButton Icon="Undo"
                              Label="Undo" />
                <AppBarButton Icon="Redo"
                              Label="Redo" />
            </CommandBar.SecondaryCommands>
            <CommandBar.Content>
                <TextBlock Text="Hello World"
                           Margin="8" />
            </CommandBar.Content>
        </CommandBar>
    </Grid>
</Page>

The app looks like this

When there is not enough space to show all primary commands they are automatically moved to the secondary commands. This also works for the AppBarElementContainer. If you want to add multiple controls to a CommandBar, don't put them in a horizontal StackPanel inside a AppBarElementContainer. Place each control in it's own AppBarElementContainer. Just like I did.

Closure

I really like the new AppBarElementContainer. I will use it a lot in my new apps running on Windows 10 (1809).

Fons

READ MORE

XAML Repeater Control

3 Comments
By Fons Sonnemans, 02-aug-2018

On Monday, the Windows Developer team announced the preview release of the Windows UI Library (WinUI). The WinUI NuGet packages contain new and popular UWP XAML controls and features which are backward-compatible on a range of Windows 10 versions, from the latest insider flights down to the Anniversary Update (1607). Windows developers will no longer need to wait for their users to adopt the latest Windows 10 release in order to provide some of the rich features provided by these packages.

Read the get started article or use this quick step-by-step guide.

Microsoft also published a Sample app on GitHub named XamlUiBasics. The dev branch already contains demos of the new SplitButton, ToggleSplitButton. DropDownButton and the Repeater control. There is not much WinUI documentation available yet so we have to figure out how it works using the sample code.

Repeater Control

The Repeater control is similar to the ItemsControl we already have. You set or databind the ItemsSource to a collection. You also have to specify the Layout and ViewGenerator properties.

The Layout property can be a StackLayout, FlowLayout, GridLayout or your custom layout. Layouts replace the ItemsPanel property of the ItemsControl. Layouts support virtualization which is very important if you have large datasets and the repeater is in a ScrollViewer. Only the visible items are painted which improves the performance a lot.

The ViewGenerator is a RecyclingViewGenerator which has a RecyclePool property and one or more (Data)Templates. The DataTemplate is used to visualize each item in the Repeater. Similar to the ItemTemplate property of the ItemsControl. The RecyclingViewGenerator also has a SelectTemplateKey event which you can use if you have multiple DataTemplates. The eventhandler should return the name (x:Name) of the DataTemplate to be used to visualize the item. This replaces the ItemTemplateSelector of the ItemsControl which if used would break the virtualization.

Demo Project

I have created a small sample project and published it on GitHub. My demo is heavily inspired by the RepeaterPage.xaml and RepeaterPage.xaml.cs from the XamlUIBasics sample. It uses the latest 17134 (1803) SDK so there is no need to upgrade to an Insiders Preview SDK.

The MainPage contains a Repeater inside a ScrollViewer

<ScrollViewer>
    <controls:Repeater x:Name="repeater"
                       ItemsSource="{x:Bind SampleDataItems}"
                       Layout="{StaticResource VerticalStackLayout}"
                       ViewGenerator="{StaticResource HorizontalElementGenerator}" />
</ScrollViewer>

This creates the following output in which each item is show in a vertical stack.

Repeater using StackLayout

The RecyclePool, StackLayout and RecyclingViewGenerator are resources inside the Page.

<controls:RecyclePool x:Key="RecyclePool1" />

<controls:StackLayout x:Name="VerticalStackLayout"
                      Orientation="Vertical"
                      ItemSpacing="8" />

<controls:RecyclingViewGenerator x:Key="HorizontalElementGenerator"
                                 RecyclePool="{StaticResource RecyclePool1}">
    <DataTemplate x:Key="hItem"
                  x:DataType="l:SampleData">
        <Grid Background="{ThemeResource SystemChromeLowColor}"
              HorizontalAlignment="Left"
              Width="{x:Bind Max}">
            <Rectangle Fill="{ThemeResource SystemAccentColor}"
                       Width="{x:Bind Value}"
                       Height="24"
                       HorizontalAlignment="Left" />
            <TextBlock Text="{x:Bind Value}"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Left"
                       Margin="4,0" />
        </Grid>
    </DataTemplate>
</controls:RecyclingViewGenerator>

 The radio buttons can be used to switch to a different Layout and ViewGenerator.

FlowLayout

In the FlowLayout all items are shown next to each other and wrap to the next line if necessary. 

Repeater using FlowLayout

The RecyclePool, FlowLayout and RecyclingViewGenerator are resources inside the Page.

<controls:RecyclePool x:Key="RecyclePool2" />
        
<DataTemplate x:Key="FlowBarTemplate"
              x:DataType="l:SampleData">
    <Border Background="{ThemeResource SystemAccentColor}"
            Width="{x:Bind Value}"
            Height="24"
            VerticalAlignment="Top">
        <TextBlock Text="{x:Bind Value}"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center" />
    </Border>
</DataTemplate>

<controls:FlowLayout x:Name="FlowLayout"
                     Orientation="Horizontal"
                     MinItemSpacing="8"
                     LineSpacing="8" />

<controls:RecyclingViewGenerator x:Key="FlowElementGenerator"
                                 RecyclePool="{StaticResource RecyclePool2}">
    <StaticResource x:Key="fItem"
                    ResourceKey="FlowBarTemplate" />
</controls:RecyclingViewGenerator>

GridLayout

In the GridLayout all items are shown in a GridView. Rectanges are shown for values below 200 others are Ellipses.

Repeater using GridLayout

The RecyclePool, GridLayout and RecyclingViewGenerator are resources inside the Page.

<controls:RecyclePool x:Key="RecyclePool3" />

<controls:GridLayout x:Name="GridLayout"
                    MinItemSpacing="8"
                    LineSpacing="8" />

<controls:RecyclingViewGenerator x:Key="GridViewGenerator"
                                 RecyclePool="{StaticResource RecyclePool3}"
                                 SelectTemplateKey="GridViewGenerator_SelectTemplateKey">
    <DataTemplate x:Name="EllipseItem"
                    x:DataType="l:SampleData">
        <Grid>
            <Ellipse Fill="{ThemeResource SystemChromeLowColor}"
                     Height="{x:Bind MaxSize}"
                     Width="{x:Bind MaxSize}"
                     VerticalAlignment="Center"
                     HorizontalAlignment="Center" />
            <Ellipse Fill="{ThemeResource SystemAccentColor}"
                     Height="{x:Bind Size}"
                     Width="{x:Bind Size}"
                     VerticalAlignment="Center"
                     HorizontalAlignment="Center" />
            <TextBlock Text="{x:Bind Value}"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Center" />
        </Grid>
    </DataTemplate>
    <DataTemplate x:Name="RectangleItem"
                  x:DataType="l:SampleData">
        <Grid>
            <Rectangle Fill="{ThemeResource SystemChromeLowColor}"
                       Height="{x:Bind MaxSize}"
                       Width="{x:Bind MaxSize}"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Center" />
            <Rectangle Fill="{ThemeResource SystemAccentColor}"
                       Height="{x:Bind Size}"
                       Width="{x:Bind Size}"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Center" />
            <TextBlock Text="{x:Bind Value}"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Center" />
        </Grid>
    </DataTemplate>
</controls:RecyclingViewGenerator>

The SelectTemplateKey eventhandler is used select the EllipseItem DataTemplate or the RectangeItem DataTemplates which are defined inside the GridViewGenerator RecyclingViewGenerator. These DataTemplates have a x:Name and not a x:Key like the one in the HorizontalElementGenerator and FlowElementGenerator.

private void GridViewGenerator_SelectTemplateKey(RecyclingViewGenerator sender, Microsoft.UI.Xaml.Controls.SelectTemplateEventArgs args) {
    args.TemplateKey = ((SampleData)args.DataContext).Value >= 200 ? nameof(this.EllipseItem) : nameof(this.RectangleItem);
}

Closure

The new Repeater control looks very useful. There are still a few things which I'm missing or unknown.

  • How do you set ItemContainerTransitions? I excpect I will have to use Composition Animations.
  • How do you use the RecyclePool correctly? I created three pools, one for each RecyclingViewGenerator.
  • How do you implement a ScrollIntoView logic. It looks like you need to place the Repeater inside an ElementTracker for that. ElementTracker enables Repeater to coordinate with ScrollViewer down-level.

We will have to wait for more docs.

Fons

READ MORE

Xaml Diff - Generate Visual State Setters

0 Comments
By Fons Sonnemans, 23-mei-2018

I have used Visual States in XAML a lot. It all started in Silverlight, now I use it in my UWP apps. I often generate them in Blend for Visual Studio using recording. Blend used to generate Storyboards but with the current version generates Setters (UWP only). This is better, makes them easier to write and read. It is a bit buggy but I expect (hope) it will be fixed soon. A lot of developers are mistakenly not using Blend. They only use Visual Studio which doesn't support the great States feature of Blend. Writing the Visual States yourself can then be a lot of work.

To help those developers I have created an app called Xaml Diff. It generates the Visual State Setters using a diff analysis of your named elements in your XAML. It is free and you can download it from the Microsoft Store.

How to use

You start by designing or writing two versions of an XAML Page or UserControl. For this demo I have created a Page with a ToggleSwitch and two Rectangles. In the first version of the page the blue Rectangle is in Column 0 of my Grid and the red Rectangle in Column 1. Both Rectangles have an x:Name to allow them to be used in the Target property of the Setter.

<Page x:Class="App27.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App27"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid Padding="12" ColumnSpacing="12" >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.ChildrenTransitions>
            <TransitionCollection>
                <ReorderThemeTransition />
            </TransitionCollection>
        </Grid.ChildrenTransitions>
        
        <ToggleSwitch Header="State1"
                      x:Name="toggleState"
                      VerticalAlignment="Top" />

        <Rectangle x:Name="rectangleBlue"
                   Grid.Row="1"
                   StrokeThickness="4"
                   Stroke="Black"
                   Fill="Blue" />

        <Rectangle x:Name="rectangleRed"
                   Grid.Row="1"
                   Grid.Column="1"
                   Fill="Red" />

    </Grid>
</Page>

You Copy & Paste the XAML of the first version of the page into the Source textbox of Xaml Diff app. In the second version of my page I swapped the column of both rectangles. I also changed the StrokeThicknes of the blue rectangle to 12. This version will be converted to a Visual State.

<Page x:Class="App27.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App27"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid Padding="12" ColumnSpacing="12" >

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.ChildrenTransitions>
            <TransitionCollection>
                <ReorderThemeTransition />
            </TransitionCollection>
        </Grid.ChildrenTransitions>
        
        <ToggleSwitch Header="State1"
                      x:Name="toggleState"
                      VerticalAlignment="Top" />

        <Rectangle x:Name="rectangleBlue"
                   Grid.Row="1"
                   StrokeThickness="12"
                   Stroke="Black"
                   Grid.Column="1"
                   Fill="Blue" />

        <Rectangle x:Name="rectangleRed"
                   Grid.Row="1"
                   Fill="Red" />

    </Grid>
</Page>

You Copy & Paste the XAML of the second version of the page into the Destination textbox of Xaml Diff app. Now you can generate the Visual State Setters using the red Generate button. With the Swap button you can swap the Source & Destination. You can use the Copy button to copy it to the Clipboard.

Using the Settings button, you have the options to include the Source in the Generated XAML. You can also add a VisualState.StateTrigger.

If you copy this generated XAML back into your IDE (Visual Studio or Blend) you only have to write the StateTrigger. In my case I data bind it to the ToggleSwitch.IsOn property, see line 15.

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="visualStateGroup1">
        <VisualState x:Name="visualState1">
            <VisualState.Setters>
                <Setter Target="rectangleBlue.(Rectangle.StrokeThickness)"
                        Value="12" />
                <Setter Target="rectangleBlue.(Grid.Column)"
                        Value="1" />
                <Setter Target="rectangleRed.(Grid.Column)"
                        Value="0" />
            </VisualState.Setters>
            <VisualState.StateTriggers>
                <StateTrigger IsActive="{x:Bind toggleState.IsOn, Mode=OneWay}" />
            </VisualState.StateTriggers>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

If you run the app and turn the ToggleSwitch on you will see the Rectangles swap column. This is animated because the Grid has a ReorderThemeTransition in the ChildrenTransitions.

Closure

I hope that the Xaml Diff can help you writing your Visual State Setters. It only works for UWP apps although Xamarin Forms 3.0 now has also Visual States support. Unfortunatly the implementation in Xamarin Forms is different. It uses Property and not Target in the Setters. Time/work for XAML Standard.

I'm open for suggestion to make this tool better. Use the Mail, Feedback or Rate buttons in the app.

Fons

READ MORE

HeaderTemplate in XAML

1 Comments
By Fons Sonnemans, 06-apr-2018

In a LOB application it is very common to have headers above an input controls (TextBox, ComboBox, Pickers, etc.) which also indicates that the data is required. This can easily be implemented in a XAML/UWP application using the HeaderTemplate property of the input controls.

Demo

In the following Page I have created a DataTemplate with the key RequiredHeaderTemplate. It contains StackPanel with 2 TextBlocks. The first is used to show the Header value. It is data bound to the Text of the TextBlock. The second is showing the '(required)' text in the accent color and a small font. The input controls in the page all have a header. The required ones also have the HeaderTemplate property set to the static resource RequiredHeaderTemplate.

<Page x:Class="HeaderTemplateDemo.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:HeaderTemplateDemo"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">
    <Page.Resources>

        <DataTemplate x:Key="RequiredHeaderTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding}" />
                <TextBlock Text="(required)"
                           Foreground="{ThemeResource SystemControlForegroundAccentBrush}"
                           FontSize="12"
                           Margin="4,0,0,1"
                           VerticalAlignment="Bottom" />
            </StackPanel>
        </DataTemplate>

    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel Orientation="Vertical"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Width="300">
            <TextBox HeaderTemplate="{StaticResource RequiredHeaderTemplate}"
                     Header="Name"
                     Margin="0,0,0,8"
                     InputScope="PersonalFullName" />
            <TextBox HeaderTemplate="{StaticResource RequiredHeaderTemplate}"
                     Header="Company"
                     Margin="0,0,0,8"
                     InputScope="PersonalFullName" />
            <TextBox Header="Email"
                     Margin="0,0,0,8"
                     InputScope="EmailNameOrAddress" />
            <ComboBox HeaderTemplate="{StaticResource RequiredHeaderTemplate}"
                      Header="Department"
                      HorizontalAlignment="Stretch"
                      Margin="0,0,0,8">
                <ComboBoxItem Content="Sales" />
                <ComboBoxItem Content="Production" />
                <ComboBoxItem Content="Marketing" />
            </ComboBox>
            <CalendarDatePicker Header="Start"
                                HeaderTemplate="{StaticResource RequiredHeaderTemplate}"
                                HorizontalAlignment="Stretch" />
        </StackPanel>
    </Grid>
</Page>

The results in the following output:

HeaderTemplate example

You can move the DataTemplate to a ResourceDictionary which makes it is available for multiple pages.

Closure

You can download the sample code using the download button below. I hope you like it.

Fons

Tags: Apps, XAML, UWP, Windows 10

READ MORE

Conditional XAML and x:Bind

0 Comments
By Fons Sonnemans, 19-mrt-2018

Recently I used Conditional XAML for the first time in one of my UWP apps. I wanted to use a ColorPicker control which is only available in the Fall Creators Update (version 1709, build 16299). The Microsoft documentation explains what Conditional XAML is really well.

Conditional XAML provides a way to use the ApiInformation.IsApiContractPresent method in XAML markup. This lets you set properties and instantiate objects in markup based on the presence of an API without needing to use code behind. It selectively parses elements or attributes to determine whether they will be available at runtime. Conditional statements are evaluated at runtime, and elements qualified with a conditional XAML tag are parsed if they evaluate to true; otherwise, they are ignored.

Conditional XAML is available starting with the Creators Update (version 1703, build 15063). To use conditional XAML, the Minimum Version of your Visual Studio project must be set to build 15063 (Creators Update) or later, and the Target Version be set to a later version than the Minimum. See Version adaptive apps for more info about configuring your Visual Studio project.

While testing my app I noticed that it can be a little tricky when you combine it with {x:Bind}. Let me explain this using a simple demo.

The Problem

To explain the problem, I have created an Universal App which uses the MVVM design pattern. I have a simple Employee model class and MainViewModel class.

class Employee {

    public string Name { get; set; }
    public double Salary { get; set; }

    public Employee(string name, double salary) {
        this.Name = name;
        this.Salary = salary;
    }

}

sealed class MainViewModel  {

    public static MainViewModel Current { get; } = new MainViewModel();

    public Employee Employee { get; } = new Employee("Fons", 2000);

    private MainViewModel() { // Singleton
    }

}

In my Page I use {x:Bind} to databind the Text of the TextBlock controls to the Name and Salary of the Employee from the MainViewModel.

<Page x:Class="ConditionalXamlAndCompiledBindingDemo.Views.Pages.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:vm="using:ConditionalXamlAndCompiledBindingDemo.ViewModels"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
          Padding="8">

        <StackPanel>
            <TextBlock Text="{x:Bind vm:MainViewModel.Current.Employee.Name}" />
            <TextBlock Text="{x:Bind vm:MainViewModel.Current.Employee.Salary}" />
        </StackPanel>

    </Grid>
</Page>

I want the StackPanel only to be used on computers running the Fall Creators Update. So, I used Conditional XAML. I declared two conditional XAML namespaces at the top of my page: fallOrHigher and lowerThanFall. I used the IsApiContractPresent(ContractName, VersionNumber) and IsApiContractNotPresent(ContractName, VersionNumber) methods to check for the UniversalApiContract version 5 (Fall Creators Update). The StackPanel is prefixed with the fallOrHigher namespace.

<Page x:Class="ConditionalXamlAndCompiledBindingDemo.Views.Pages.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:vm="using:ConditionalXamlAndCompiledBindingDemo.ViewModels"
      xmlns:fallOrHigher="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,5)"
      xmlns:lowerThanFall="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractNotPresent(Windows.Foundation.UniversalApiContract,5)"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
          Padding="8">

        <fallOrHigher:StackPanel>
            <TextBlock Text="{x:Bind vm:MainViewModel.Current.Employee.Name}" />
            <TextBlock Text="{x:Bind vm:MainViewModel.Current.Employee.Salary}" />
        </fallOrHigher:StackPanel>

    </Grid>
</Page>

When I tested the application it seemed to work fine, the StackPanel was visible. But you are tricked. You also have to test it on an older version of Windows (1703). To do/simulate this I changed the IsApiContractPresent() method of the fallOrHigher namespace into an IsApiContractNotPresent(). The application crashed with a NullReferenceException.

The Solution

Examining the exception I learned that it was caused by the generated code of the {x:Bind}.

System.NullReferenceException

The StackTrace shows it occured in the Set_Windows_UI_Xaml_Controls_TextBlock_Text() method which is automatically generated by the {x:Bind}. That's why it is called compiled binding. The solution is easy, don't generate this code. We can do this by adding the fallOrHigher namespace prefix in front of the databound properties of the inner controls of the StackPanel. Or we can add the prefix in front of the all inner controls which use {x:Bind}. This makes the {x:Bind} conditional and solves our problem.

<fallOrHigher:StackPanel>
    <TextBlock fallOrHigher:Text="{x:Bind vm:MainViewModel.Current.Employee.Salary}" />    
    <fallOrHigher:TextBlock Text="{x:Bind vm:MainViewModel.Current.Employee.Name}" />
</fallOrHigher:StackPanel>

When you test your app you won't get the exception and the StackPanel isn't there. Don't forget to change the IsApiContractNotPresent() method of the fallOrHigher namespace back into the IsApiContractPresent().

Closure

Testing Conditional XAML on correct use is very important. It's not difficult but easily overlooked. Conditional XAML is very powerful and I will use it a lot in my apps. The 1803 version of the SDK has some new properties and the TreeView control which I want to start using.

You can download the sample code using the download button below.

Fons

Tags: Apps, XAML, UWP, Windows 10

READ MORE

TechDays 2017 - New XAML UWP Features in Windows 10 Fall Creators Update

0 Comments
By Fons Sonnemans, 25-okt-2017

Op het TechDays 2017 event van Microsoft heb ik een sessie gepresenteerd met de titel 'New XAML UWP Features in Windows 10 Fall Creators Update'. Via deze blog wil ik de video, presentatie en source code met u delen.

Video

Presentatie

New XAML/UWP features in Windows 10 Fall Creators Update from Fons Sonnemans

Source code

De source code van de demo applicatie heb ik op GitHub gepubliceerd.

READ MORE

Replace a RepositionThemeTransition with an ImplicitAnimation

0 Comments
By Fons Sonnemans, 14-jul-2017

Windows (UWP) apps use animations to enhance the user interface or to make it more attractive without annoying your users. One way you can do this is to apply animated transitions to UI so that when something enters or leaves the screen or otherwise changes, the animation draws the attention of the user to the change. As you can read in the documentation (Animation overview) you can use the reposition animations (RepositionThemeTransition) to move an element into a new position. For example changing the position (Row/Column) of an item in a Grid. 

RepositionThemeTransition

The next XAML snippet has a Grid with two columns and two rows. The red Rectangle is shown in the top left corner (column 0 and row 0). The Grid has a RepositionThemeTransition in the ChildrenTransitions property. The Button is used to reposition the Rectangle.

<Button Content="Reposition Left"
        Click="ButtonLeft_Click" />

<Grid Background="Aqua"
      Grid.Row="1"
      Margin="8">
    <Grid.ChildrenTransitions>
        <TransitionCollection>
            <RepositionThemeTransition />
        </TransitionCollection>
    </Grid.ChildrenTransitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="1*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="1*" />
        <RowDefinition Height="1*" />
    </Grid.RowDefinitions>

    <Rectangle x:Name="rectLeft"
               Margin="8"
               Fill="Red" />
</Grid>

In the ButtonLeft_Click method the position of the rectangle changes. From left to right and from top to bottom. This is done by changing the Grid.Row and Grid.Column attached properties.

private void ButtonLeft_Click(object sender, RoutedEventArgs e) {
    Reposition(rectLeft);
}

private void Reposition(FrameworkElement element) {
    int row = Grid.GetRow(element);
    int col = Grid.GetColumn(element);
    if (col == 0) {
        col++;
    } else {
        if (row == 0) {
            row++;
        } else {
            row = 0;
        }
        col = 0;
    }
    Grid.SetRow(element, row);
    Grid.SetColumn(element, col);
}

A RepositionThemeTransition has a property IsStaggeringEnabled that determines whether the transition staggers rendering of multiple items, or renders all items at once. It does not have a Duration or Easing properties. The default duration of 300 milliseconds is sometimes too fast for me.

Implicit Animations

To solve my missing Duration property problem I came with the following solution. You can replace the RepositionThemeTransition with an Implicit Animations. Implicit Animations were added in the Windows 10 Anniversary Update (Build 10586, Nov 2016) as part of the Composition API.

The following XAML snippet has a button which also changes the position (Grid.Row and Grid.Column properties) of a Rectangle in a Grid. The ComboBox is used to select the Duration of the animation. There is no RepostionThemeTransition in this Grid.

<Button Content="Reposition Right"
        Click="ButtonRight_Click"
        Margin="8,8,8,0"
        VerticalAlignment="Bottom"
        Grid.Column="1" />

<ComboBox x:Name="comboBoxDuration"
            SelectionChanged="comboBoxDuration_SelectionChanged"
            Header="Duration"
            Margin="8,8,8,0"
            SelectedIndex="1"
            Grid.Column="1"
            HorizontalAlignment="Right">
    <ComboBoxItem>150</ComboBoxItem>
    <ComboBoxItem>300</ComboBoxItem>
    <ComboBoxItem>600</ComboBoxItem>
    <ComboBoxItem>900</ComboBoxItem>
    <ComboBoxItem>1200</ComboBoxItem>
</ComboBox>

<Grid Background="Aqua"
      Margin="8"
      Grid.Row="1"
      Grid.Column="1">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="1*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="1*" />
        <RowDefinition Height="1*" />
    </Grid.RowDefinitions>

    <Rectangle x:Name="rectRight"
               Margin="8"
               Fill="Red" />
</Grid>

The real work is done in the InitComposition method which is called from the MainPage_Loaded eventhandler. This method creates the ImplicitAnimation. If you don't know how it works you should read the robmikh blog 'Exploring Implicit Animations'. I used his code but added an easing function (line 48) and a flexible Duration (line 60). The Duration can be selected by the user using the ComboBox. In his sample code he repositioned a self-created SpriteVisual by setting the Offset. My code creates the visual from the Rectangle created in XAML (line 46). I expected that it would only work if you change the Offset of the visual but luckily it also works when you change the Grid.Column or Grid.Row attached properties. It even works when you resize the page.

public sealed partial class MainPage : Page {
    private Compositor _compositor;
    private Visual _visual;
    private Vector3KeyFrameAnimation _offsetAnimation;
    private ScalarKeyFrameAnimation _rotationAnimation;

    public MainPage() {
        this.InitializeComponent();
        this.Loaded += this.MainPage_Loaded;
        this.Unloaded += this.MainPage_Unloaded;
    }

    private void MainPage_Unloaded(object sender, RoutedEventArgs e) {
        _visual.Dispose();
    }

    private void MainPage_Loaded(object sender, RoutedEventArgs e) {
        // InitComposition in Loaded event so the Size of the Visual is set
        InitComposition(rectRight);
    }

    private void ButtonRight_Click(object sender, RoutedEventArgs e) {
        Reposition(rectRight);
    }

    private void Reposition(FrameworkElement element) {
        int row = Grid.GetRow(element);
        int col = Grid.GetColumn(element);
        if (col == 0) {
            col++;
        } else {
            if (row == 0) {
                row++;
            } else {
                row = 0;
            }
            col = 0;
        }
        Grid.SetRow(element, row);
        Grid.SetColumn(element, col);
    }

    private void InitComposition(FrameworkElement element) {
        if (ApiInformation.IsTypePresent("Windows.UI.Xaml.Hosting.ElementCompositionPreview")) {

            _compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;

            var easing = _compositor.CreateCubicBezierEasingFunction(
                    new Vector2(0.3f, 0.3f),
                    new Vector2(0.0f, 1.0f)
            );
            //var easing = _compositor.CreateLinearEasingFunction();

            _visual = ElementCompositionPreview.GetElementVisual(element);

            // Create animation
            _offsetAnimation = _compositor.CreateVector3KeyFrameAnimation();
            _offsetAnimation.Target = nameof(Visual.Offset);
            _offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", easing);
            _offsetAnimation.Duration = TimeSpan.FromMilliseconds(SelectedDuration);

            // Set trigger + assign animation
            var implicitAnimations = _compositor.CreateImplicitAnimationCollection();
            implicitAnimations[nameof(Visual.Offset)] = _offsetAnimation;
            _visual.ImplicitAnimations = implicitAnimations;
        }
    }

    private void comboBoxDuration_SelectionChanged(object sender, SelectionChangedEventArgs e) {
        if (_offsetAnimation != null) {
            _offsetAnimation.Duration = TimeSpan.FromMilliseconds(SelectedDuration);
        }
        if (_rotationAnimation != null) {
            _rotationAnimation.Duration = _offsetAnimation.Duration;
        }
    }

    public int SelectedDuration => Convert.ToInt32((comboBoxDuration.SelectedItem as ComboBoxItem).Content);

Result

The result of this all can be seen in the folowing video (GIF) of my demo app.  The left Rectangle uses a RepositionThemeTransition, the right one uses an Implicit Animation. The ComboBox can be used to set the duration of the Implicit Animation.

Multiple Animations using an AnimationGroup

I also experimented with a solution in which I would not only animate the position of the Rectangle but also a 360 degree rotation. I had to determine the CenterPoint of the visual, also when it resizes (lines 11-17). I created the rotation animation and added it together with the offset animation to an animation group. This group is assigned to the ImplicitAnimations property of the Visual.

private void InitComposition(FrameworkElement element) {
    if (ApiInformation.IsTypePresent("Windows.UI.Xaml.Hosting.ElementCompositionPreview")) {
        _compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;

        var easing = _compositor.CreateCubicBezierEasingFunction(
                new Vector2(0.3f, 0.3f),
                new Vector2(0.0f, 1.0f)
        );

        _visual = ElementCompositionPreview.GetElementVisual(element);

        // Set CenterPoint to the center, also when resized
        SizeChangedEventHandler fp = (sender, e) =>
            _visual.CenterPoint = new Vector3((float)element.ActualWidth / 2f,
                                                (float)element.ActualHeight / 2f, 0.0f);
        element.SizeChanged += fp;
        fp.Invoke(null, null);

        // Create animation
        _offsetAnimation = _compositor.CreateVector3KeyFrameAnimation();
        _offsetAnimation.Target = nameof(Visual.Offset);
        _offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", easing);
        _offsetAnimation.Duration = TimeSpan.FromMilliseconds(SelectedDuration);

        _rotationAnimation = _compositor.CreateScalarKeyFrameAnimation();
        _rotationAnimation.Target = nameof(Visual.RotationAngleInDegrees);
        _rotationAnimation.InsertKeyFrame(0.0f, 0.0f);
        _rotationAnimation.InsertKeyFrame(1.0f, 360.0f);
        _rotationAnimation.Duration = _offsetAnimation.Duration;

        var animationGroup = _compositor.CreateAnimationGroup();
        animationGroup.Add(_offsetAnimation);
        animationGroup.Add(_rotationAnimation);

        // Set trigger + assign animation
        var implicitAnimations = _compositor.CreateImplicitAnimationCollection();
        implicitAnimations[nameof(Visual.Offset)] = animationGroup;
        _visual.ImplicitAnimations = implicitAnimations;
    }
}

This experiment seems to work OK but I noticed that the rotation also occurs when my page resizes. This because the Offset of the visual changes which triggers the Implicit Animations. I'm not sure if this is something I want.

Closure

Although it works it is not a very simple solution for a small problem. I hope Microsoft will add a Duration property in the near future. I prefer to work declarative.

I have published the updated code on GitHub. I hope you like it.

Fons

READ MORE

New ColumnSpacing and RowSpacing properties for Grid

1 Comments
By Fons Sonnemans, 03-jul-2017

Last week I blogged about the new Spacing property in StackPanel that is introduced in the new Windows 10 SDK Preview Build 16225 (Fall Creators Update/CU2). Today I want to explain the new ColumnSpacing and RowSpacing of the Grid control. The RowSpacing can be used to set the amount of space between each row. The ColumnSpacing can be used to set the amount of space between each column. It allows you to create gutters between the rows and columns.

Demo

In the next demo page I have a Grid with 4 columns and 3 rows. I have placed 3 Rectangles in it. The first (Red) in the top left cell, the second (white) in row 1 + column 1, the third (blue) in row 2 + column 2 with a columnspan of 2. The ColumnSpacing is set to 48 and the RowSpacing to 24.

<Page
    x:Class="App4.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App4"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="LightBlue" RowSpacing="24" ColumnSpacing="48">
        <Grid.RowDefinitions>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Rectangle Fill="Red"  />
        <Rectangle Fill="White" 
                   Grid.Row="1" Grid.Column="1"  />
        <Rectangle Fill="Blue"  
                   Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2"  />
    </Grid>
</Page>

This renders to the following result. There is a gutter of 24 pixels between row 0 and 1 and between row 1 and 2. There is a gutter of 48 pixels between column 0 and 1, between column 1 and 2 and between column 2 and 3. This last gutter is not really visible because the blue rectangle has a columnspan of 2 which means that it also draws itself over the gutter.

XAML Grid Spacing example

If you remove these properties it will render like this. The white rectangle is connected in the corner with the 2 other rectangles.

XAML Grid example with no spacing

XAML Designer Issues

The XAML Designer in Visual Studio 2017 Preview and Blend don't render the Grid correctly yet. But I'm sure that Microsoft will fix that too.

XAML Designer screenshot

I find the new RowSpacing and ColumnSpacing properties very useful. It removes the need to set the Margin on child elements. I hope you like it too.

Fons

READ MORE

New Spacing property in StackPanel

0 Comments
By Fons Sonnemans, 30-jun-2017

A few days ago Microsoft released the new Windows 10 SDK Preview Build 16225 (Fall Creators Update/CU2). While browsing the 'API Updates and Additions' I noticed there is a new Spacing property for the StackPanel control. You can use it to set the amount of space between each child element. The StackLayout control of Xamarin Forms already had this property. This will make it easier to add this property to the XAML Standard.

Demo

In the next demo page I have created a StackPanel with 3 Rectangles in it. The Margin is set to 40, the BorderThickness is set to 8, the Padding is set to 32 and the Spacing is set to 24.

<Page
    x:Class="App4.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App4"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="LightBlue">
        <StackPanel Margin="40" BorderThickness="8" BorderBrush="Black" 
                    Padding="32" Spacing="24" Background="LightGray">
            <Rectangle Height="80" Fill="Red"  />
            <Rectangle Height="80" Fill="White"   />
            <Rectangle Height="80" Fill="Blue"  />
        </StackPanel>
    </Grid>
</Page>

This renders to the following result. I have added the text 'Margin', 'Padding' and 'Spacing' to the illustration to make it clear where it used for.

XAML StackPanel Spacing example

Explanation of the different parts:

  • Margin - Clears an area outside the border.
  • BorderThickness - A border that goes around the padding
  • Padding - Clears an area around the content (inside the border).
  • Spacing- The space between each child element

I find this new Spacing property very useful. It removes the need to set the Margin on each child element. I hope you like it too.

Fons

READ MORE

Paging in ASP.NET Core MVC and EntityFramework Core

67 Comments
By Fons Sonnemans, 29-mrt-2017

Paging, sorting and filtering are common features in websites. Microsoft has written a tutorial how to implement these features in ASP.NET Core MVC with Entity Framework Core. The described solution works but I found it a bit too primitive. It triggered me to create a more powerful solution. Before you can start using my solution you should first read this tutorial, it explains how you can add Entity Framework Core to an ASP.NET Core MVC application.

Project Setup

My WebApplication is an ASP.NET Core Web Application (version 1.1) which uses EntityFramework Core 1.1. I use a commonly used Northwind SQL Server sample database which I created using the following tutorial. I have scaffold the Northwind database and created the controllers for the Suppliers and Products. Read this tutorial to learn how. I have added some extra navigation hyperlinks in the _Layout.cshtml file for the SuppliersController and ProductsController (Action Index).

The Index.cshtml file from the Views/Suppliers folder looks like this. I made some small adjustments like changing the h2 to an h1 and renaming the text 'Index' to 'Suppliers'. The most obvious change is that I removed some of the columns from the table to make it less wide.

@model IEnumerable<WebApplication8.Models.Database.Suppliers>

@{
    ViewData["Title"] = "Suppliers";
}

<h1>Suppliers</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table table-striped">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.CompanyName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ContactName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.City)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model) {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.CompanyName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ContactName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Address)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.SupplierId">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.SupplierId">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.SupplierId">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

When you browse for the Suppliers Index page (http://localhost:2309/Suppliers) you get the following output. It shows you all suppliers and that list can be very long. Let's add Paging so we can limit the number of suppliers in the list.

Adding Paging

In the earlier mentioned tutorial a PaginatedList<T> class was used. My solution uses a similar approach but I named it PagingList<T>. This class and some other helper classes are packed in a NuGet package named ReflectionIT.Mvc.Paging. You can install the package using the NuGet browser in Visual Studio or by running the 'Install-Package ReflectionIT.Mvc.Paging' command from the Package Manager Console.

After installation you can add the paging services in the ConfigureServices() method of the Startup class. Add the services.AddPaging() call as shown below. This call will register an EmbeddedFileProvider which is used for rendering the View for the Pager ViewComponent. The View will be loaded from the ReflectionIT.Mvc.Paging assembly if it can not be found in your WebApplication.

public void ConfigureServices(IServiceCollection services)
{
    services.AddEntityFramework()
        .AddEntityFrameworkSqlServer()
        .AddDbContext<Models.Database.NorthwindContext>(options =>
            options.UseSqlServer(Configuration["Data:Northwind:ConnectionString"]));

    // Add framework services.
    services.AddMvc();

    services.AddPaging();
}

Next I modified the Index() method from the SuppliersController class. I added an optional 'page' parameter with the default value 1. I defined a query in which I retrieve the Suppliers from the context (NorthwindContext) with the AsNoTracking() method and an OrderBy(). The AsNoTracking() turns off change tracking which improves the performance. The OrderBy() is required to support Paging (Skip() & Take()) in EF. This query is used to create the PagingList. The page size is set to 10. This is the model which is passed to the View. The PagingList will fetch the data from the database asynchronously.

// GET: Suppliers
//public async Task<IActionResult> Index() {
//    return View(await _context.Suppliers.ToListAsync());
//}

public async Task<IActionResult> Index(int page = 1) {
    var qry = _context.Suppliers.AsNoTracking().OrderBy(p => p.CompanyName);
    var model = await PagingList.CreateAsync(qry, 10, page);
    return View(model);
}

The Index.cshtml view must also be modified. I changed the model type (line 1), added a using (line 2) and an addTagHelper (line 3). Lines 2 and 3 could also be moved to the _ViewImports.cshtml file which would add these lines to every view. I also added a Pager above the table. This is done by invoking the Pager View Component and passing the Model as the pagingList. It is placed inside a <nav> element with an aria-label which is used by screen readers. I have done the same below the <table> but there I used a new feature of ASP.NET Core 1.1. The View Component is invoked as a Tag Helper. It uses the <vc /> element and the class and parameters are translated to lower kebab case.  For more info read this article.

@model ReflectionIT.Mvc.Paging.PagingList<WebApplication8.Models.Database.Suppliers>
@using ReflectionIT.Mvc.Paging
@addTagHelper *, ReflectionIT.Mvc.Paging

@{
    ViewData["Title"] = "Suppliers";
}

<h2>Suppliers</h2>

<nav aria-label="Suppliers navigation example">
    @await this.Component.InvokeAsync("Pager", new { pagingList = this.Model })
</nav>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table table-striped">
   ...
</table>

<nav aria-label="Suppliers navigation example">
    <vc:pager paging-list="@Model" />
</nav>

This results in the following view in which you see two pagers which use bootstrap markup.

 

Adding Sorting

For sorting I added an extra sortExpression parameter to the Index() method of the SuppliersController class. The parameter is optional and uses the CompanyName as the default sort expression in case none is supplied. The OrderBy() method is removed from the query because the PagingList will take care of the ordering. The sortExpression and the default sort expression are now required to create the PageList model.

public async Task<IActionResult> Index(int page = 1, 
                                       string sortExpression = "CompanyName") {

    var qry = _context.Suppliers.AsNoTracking();
    var model = await PagingList.CreateAsync(
                            qry, 10, page, sortExpression, "CompanyName");
    return View(model);
}

In the View I modified the header of the table. I replaced the DisplayNameFor() calls by SortableHeaderFor() calls. I pass the Model as an extra parameter. This is not required but if you do you will get nice Up and Down indicators (glyphs) in the headers when you sort ascending or descending. You can also specify the SortColumn if you want a different one as the model property. In this example I used this for the City column. There I added the CompanyName as a second column to sort on.

<table class="table table-striped">
    <thead>
        <tr>
            <th>
                @Html.SortableHeaderFor(model => model.CompanyName, this.Model)
            </th>
            <th>
                @Html.SortableHeaderFor(model => model.ContactName, this.Model)
            </th>
            <th>
                @Html.SortableHeaderFor(model => model.Address, this.Model)
            </th>
            <th>
                @Html.SortableHeaderFor(model => model.City, "City, CompanyName", this.Model)
            </th>
            <th></th>
        </tr>
    </thead>

This results in the following view in which you can sort the table by clicking the headers of the columns. An up or down indicator shows you weather you are sorting ascending or descending.

Adding Filtering

To show filtering I will switch to the Products table, it has more records. The Index() method of the ProductsController has a filter parameter (besides page and sortExpression). The query is defined with an extra AsQueryable() method call. This method returns the the type IQueryable<Products>. This allowed me to append an extra Where "clause" to it when the filter is not empty.  In this example I used a Contains() which translates to a like '%...%' condition.

You have to set the RouteValue property of the model with all conditions used in the filter. This is used to build the correct URL in the pager and table headers.

public async Task<IActionResult> Index(string filter, int page = 1, 
                                       string sortExpression = "ProductName") {

    var qry = _context.Products.AsNoTracking()
        .Include(p => p.Category)
        .Include(p => p.Supplier)
        .AsQueryable();

    if (!string.IsNullOrWhiteSpace(filter)) {
        qry = qry.Where(p => p.ProductName.Contains(filter));
    }

    var model = await PagingList.CreateAsync(
                                 qry, 10, page, sortExpression, "ProductName");

    model.RouteValue = new RouteValueDictionary {
        { "filter", filter}
    };

    return View(model);
}

I added a form in the Index.cshtml with an input named filter and a submit button which has the method set to GET. This will execute the Index action of the controller.

@model ReflectionIT.Mvc.Paging.PagingList<WebApplication8.Models.Database.Products>
@using ReflectionIT.Mvc.Paging
@addTagHelper *, ReflectionIT.Mvc.Paging

@{
    ViewData["Title"] = "Products";
}

<h2>Products</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form method="get" class="form-inline">
    <input name="filter" class="form-control" placeholder="filter" 
           value="@Model.RouteValue["Filter"]" />
    <button type="submit" class="btn btn-info">
        <span class="glyphicon glyphicon-search" aria-hidden="true"></span> Search
    </button>
</form>

<nav aria-label="Products navigation example">
    <vc:pager paging-list="@Model" />
</nav>

<table class="table table-striped">
    <thead>
        <tr>
            <th>
                @Html.SortableHeaderFor(model => model.ProductName, this.Model)
            </th>
            <th>
                @Html.SortableHeaderFor(model => model.Supplier.CompanyName, 
                               "Supplier.CompanyName, ProductName", this.Model)
            </th>
            <th>
                @Html.SortableHeaderFor(model => model.Category.CategoryName, 
                              "Category.CategoryName, ProductName", this.Model)
            </th>
            <th>
                @Html.SortableHeaderFor(model => model.UnitPrice, this.Model)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>

This results in the following view in which you can filter on ProductName and you have paging and sorting.

Customize

You can customize this solutions in a few ways. You can for instance call the AddPaging method which you can use to set the PagingOptions. This allows you to specify which View is used for the Pager ViewComponent. By default a pager based on Bootstrap3 is used. But there is also already a Bootstrap4 view available. Bootstrap4 doesn't support glyphs any more so if you switch to it you also have to specify alternatives for the Up/Down indicators used in the sortable headers of the table.

services.AddPaging(options => {
    options.ViewName = "Bootstrap4";
    options.HtmlIndicatorDown = " <span>&darr;</span>";
    options.HtmlIndicatorUp = " <span>&uarr;</span>";
});

You can also create your own Pager view if you want to. You store it in the folder Views\Shared\Components\Pager. The easiest way to create a custom view is by coping the default Bootstrap3.cshtml from Github. You can remove the Bootstrap classes and add your own or you can generate your own html buttons.

You can also render the Pager using the Html.Partial() Html Helper instead of using the Pager ViewComponent. The following snippet shows the 'SmallPager.cshtml' which I stored in the Views/Shared folder. When you store it in this folder you can access it from all views.

<nav aria-label="Products navigation example">
    @Html.Partial("SmallPager", this.Model)
</nav>

Closure

You can download my sample app using the Download button below. I have published the code of the Pager on GitHub. It also contains a sample webapp which uses Bootstrap4. It's open source. So if you create a better pager please do a pull-request.

Fons

 

READ MORE

All postings/content on this blog are provided "AS IS" with no warranties, and confer no rights. All entries in this blog are my opinion and don't necessarily reflect the opinion of my employer or sponsors. The content on this site is licensed under a Creative Commons Attribution By license.