Quantcast
Channel: Reflection IT Blog
Viewing all 1291 articles
Browse latest View live

XAML Animated Headered TextBox Style

$
0
0

A while ago I saw a beautiful effect in an Android app which I was using. It had textboxes with a placeholder text. This placeholder text moved to the header when you enter the first character in the textbox. This triggered me to do the same in a Windows 10 XAML app (UWP). This is a video of the result.

AnimatedHeaderedTextBoxStyle demo video

AnimatedHeaderedTextBoxStyle

My solution is quite simple. I created a Style for a TextBox which has an extra StateGroup named 'HeaderStates'. There are two States in this group named Empty and NotEmpty. The HeaderContentPresenter is moved using TranslateX and TranslateY setters to the correct position. The states are triggered using TemplatedParent databinding to the Text property. I have used a converter which returns true or false when the text is empty or not. I have also changed the BorderThickness property of the Style. It now has only a bottom line and is not boxed any more.

<Style x:Key="AnimatedHeaderedTextBoxStyle"
        TargetType="TextBox"><Setter Property="MinWidth"
            Value="{ThemeResource TextControlThemeMinWidth}" /><Setter Property="MinHeight"
            Value="{ThemeResource TextControlThemeMinHeight}" /><Setter Property="Foreground"
            Value="{ThemeResource TextControlForeground}" /><Setter Property="Background"
            Value="{ThemeResource TextControlBackground}" /><Setter Property="BorderBrush"
            Value="{ThemeResource TextControlBorderBrush}" /><Setter Property="SelectionHighlightColor"
            Value="{ThemeResource TextControlSelectionHighlightColor}" /><Setter Property="BorderThickness"
            Value="0,0,0,2" /><Setter Property="FontFamily"
            Value="{ThemeResource ContentControlThemeFontFamily}" /><Setter Property="FontSize"
            Value="{ThemeResource ControlContentThemeFontSize}" /><Setter Property="ScrollViewer.HorizontalScrollMode"
            Value="Auto" /><Setter Property="ScrollViewer.VerticalScrollMode"
            Value="Auto" /><Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
            Value="Hidden" /><Setter Property="ScrollViewer.VerticalScrollBarVisibility"
            Value="Hidden" /><Setter Property="ScrollViewer.IsDeferredScrollingEnabled"
            Value="False" /><Setter Property="Padding"
            Value="{ThemeResource TextControlThemePadding}" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="TextBox"><Grid><Grid.Resources><Style x:Name="DeleteButtonStyle"
                                TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid x:Name="ButtonLayoutGrid"
                                                BorderBrush="{ThemeResource TextControlButtonBorderBrush}"
                                                BorderThickness="{TemplateBinding BorderThickness}"
                                                Background="{ThemeResource TextControlButtonBackground}"><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Normal" /><VisualState x:Name="PointerOver"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBackgroundPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBorderBrushPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                                            Storyboard.TargetName="GlyphElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonForegroundPointerOver}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Pressed"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBackgroundPressed}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBorderBrushPressed}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                                            Storyboard.TargetName="GlyphElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonForegroundPressed}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Disabled"><Storyboard><DoubleAnimation Duration="0"
                                                                                To="0"
                                                                                Storyboard.TargetProperty="Opacity"
                                                                                Storyboard.TargetName="ButtonLayoutGrid" /></Storyboard></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><TextBlock x:Name="GlyphElement"
                                                        AutomationProperties.AccessibilityView="Raw"
                                                        Foreground="{ThemeResource TextControlButtonForeground}"
                                                        FontStyle="Normal"
                                                        FontSize="12"
                                                        FontFamily="{ThemeResource SymbolThemeFontFamily}"
                                                        HorizontalAlignment="Center"
                                                        Text="&#xE10A;"
                                                        VerticalAlignment="Center" /></Grid></ControlTemplate></Setter.Value></Setter></Style></Grid.Resources><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="*" /></Grid.RowDefinitions><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Disabled"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="HeaderContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlHeaderForegroundDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBackgroundDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBorderBrushDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlForegroundDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="PlaceholderTextContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlPlaceholderForegroundDisabled}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Normal" /><VisualState x:Name="PointerOver"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBorderBrushPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBackgroundPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="PlaceholderTextContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlPlaceholderForegroundPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlForegroundPointerOver}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Focused"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="PlaceholderTextContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlPlaceholderForegroundFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBackgroundFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBorderBrushFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlForegroundFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="RequestedTheme"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="Light" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState></VisualStateGroup><VisualStateGroup x:Name="ButtonStates"><VisualState x:Name="ButtonVisible"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility"
                                                                    Storyboard.TargetName="DeleteButton"><DiscreteObjectKeyFrame KeyTime="0"><DiscreteObjectKeyFrame.Value><Visibility>Visible</Visibility></DiscreteObjectKeyFrame.Value></DiscreteObjectKeyFrame></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="ButtonCollapsed" /></VisualStateGroup><VisualStateGroup x:Name="HeaderStates"><VisualStateGroup.Transitions><VisualTransition GeneratedDuration="0:0:0.35"><VisualTransition.GeneratedEasingFunction><CircleEase EasingMode="EaseInOut" /></VisualTransition.GeneratedEasingFunction></VisualTransition></VisualStateGroup.Transitions><VisualState x:Name="NotEmpty"><VisualState.Setters><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
                                            Value="0" /><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateX)"
                                            Value="0" /></VisualState.Setters><VisualState.StateTriggers><StateTrigger IsActive="{Binding Text, Converter={StaticResource IsNotEmptyConverter}, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /></VisualState.StateTriggers></VisualState><VisualState x:Name="Empty"><VisualState.Setters><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
                                            Value="31" /><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateX)"
                                            Value="11" /></VisualState.Setters><VisualState.StateTriggers><StateTrigger IsActive="{Binding Text, Converter={StaticResource IsEmptyConverter}, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /></VisualState.StateTriggers></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><Border x:Name="BorderElement"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}"
                            Grid.ColumnSpan="2"
                            Grid.Row="1"
                            Grid.RowSpan="1" /><ContentPresenter x:Name="HeaderContentPresenter"
                                        Grid.ColumnSpan="2"
                                        ContentTemplate="{TemplateBinding HeaderTemplate}"
                                        Content="{TemplateBinding Header}"
                                        Foreground="{ThemeResource TextControlHeaderForeground}"
                                        FontWeight="Normal"
                                        Margin="0,0,0,8"
                                        Grid.Row="0"
                                        Visibility="Collapsed"
                                        x:DeferLoadStrategy="Lazy"
                                        RenderTransformOrigin="0.5,0.5"><ContentPresenter.RenderTransform><CompositeTransform /></ContentPresenter.RenderTransform></ContentPresenter><ScrollViewer x:Name="ContentElement"
                                    AutomationProperties.AccessibilityView="Raw"
                                    HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
                                    HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
                                    IsTabStop="False"
                                    IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
                                    IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
                                    IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
                                    Margin="{TemplateBinding BorderThickness}"
                                    Padding="{TemplateBinding Padding}"
                                    Grid.Row="1"
                                    VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
                                    VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
                                    ZoomMode="Disabled" /><ContentControl x:Name="PlaceholderTextContentPresenter"
                                    Grid.ColumnSpan="2"
                                    Content="{TemplateBinding PlaceholderText}"
                                    Foreground="{ThemeResource TextControlPlaceholderForeground}"
                                    IsHitTestVisible="False"
                                    IsTabStop="False"
                                    Margin="{TemplateBinding BorderThickness}"
                                    Padding="{TemplateBinding Padding}"
                                    Grid.Row="1" /><Button x:Name="DeleteButton"
                            AutomationProperties.AccessibilityView="Raw"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Grid.Column="1"
                            FontSize="{TemplateBinding FontSize}"
                            IsTabStop="False"
                            Margin="{ThemeResource HelperButtonThemePadding}"
                            MinWidth="34"
                            Grid.Row="1"
                            Style="{StaticResource DeleteButtonStyle}"
                            Visibility="Collapsed"
                            VerticalAlignment="Stretch" /></Grid></ControlTemplate></Setter.Value></Setter></Style>

The converter is very simple. It has an extra IsInvertered property to invert a True to a False.

public class EmptyStringToTrueConverter : IValueConverter {
    public bool IsInverted { get; set; }

    public object Convert(object value, Type targetType, object parameter, string language) {
        var txt = (string)value;
        return string.IsNullOrEmpty(txt) ^ IsInverted;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language) {
        return DependencyProperty.UnsetValue;
    }
}

The converteres are registered using the following XAML.

<local:EmptyStringToTrueConverter x:Key="IsNotEmptyConverter" IsInverted="True" /><local:EmptyStringToTrueConverter x:Key="IsEmptyConverter" />

Usage

My example page which I used in the video is shown below. All TextBoxes have the Style property set to AnimatedHeaderedTextBoxStyle and off course a Header property. I have set the IsSpellCheckEnable property to False because I don't want spellchecking on names and email addresses.  

<Page x:Class="App121.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App121"
      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}"><StackPanel Width="300"
                    Margin="0,12"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Center"><TextBox Header="Firstname"
                     Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                     IsSpellCheckEnabled="False"
                     InputScope="PersonalFullName"/><TextBox Header="Lastname"
                     Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                     IsSpellCheckEnabled="False"
                     Margin="0,8"
                     InputScope="PersonalFullName" /><TextBox Header="E-mail"
                     Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                     IsSpellCheckEnabled="False"
                     InputScope="EmailNameOrAddress" /><Button Content="Next"
                    Margin="0,16,0,0" /></StackPanel></Grid></Page>

I have placed the AnimatedHeaderedTextBoxStyle, the registration of the converters and a DeleteButtonStyle into ResourceDictionary named AnimatedHeaderedTextBoxStyle.xaml. The DeleteButtonStyle was generated by Blend when I created the Style by 'Editing a Copy...' of the Template for an existing TextBox.

Edit a Copy

In have registered the ResourceDictionary from the App.xaml which makes it available for in all pages and controls in this application.

<Application
    x:Class="App121.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App121"
    RequestedTheme="Light"><Application.Resources><ResourceDictionary><ResourceDictionary.MergedDictionaries><ResourceDictionary Source="AnimatedHeaderedTextBoxStyle.xaml" /></ResourceDictionary.MergedDictionaries></ResourceDictionary></Application.Resources></Application>

The easiest way to add this Resource Dictionary as a MergedDictionaries in the App.xaml is to use Blend. Open the Resources Window and right click the App.xaml file. Then select 'Link to Resource Dictionary' and check the AnimatedHeaderedTextBoxStyle.xaml file.

Link to ResourceDictionary

The code

I have published my code on GitHub. I hope you like it.

Fons


XAML Animated Headered TextBox Style (Part 2)

$
0
0

Last week I wrote a blog item about the XAML Animated Headered TextBox Style. I got a lot of reactions on Twitter. Pieter Otten challenged me into adjusting my solution to make it conform the Material Design guidelines. In Android the header (Label) of the TextBox (Text Field) animates on Focus not when you enter a value. I tried to implement this by adjusting my style but that didn't work. I had to introduce an extra behavior to get it to work. See the result in this video.

AnimatedHeaderedTextBoxStyle demo video which works on Focus

MoveHeaderOnFocusBehavior

The MoveHeaderOnFocusBehavior is used to set the Visual States (Empty and NotEmpty) of the associated TextBox when it is loaded, gets the focus, loses the focus or when the text changes. This Behavior was build by subclassing the Behavior<T> class from the Microsoft.Xaml.Behaviors.Uwp.Managed NuGet package.

public class MoveHeaderOnFocusBehavior : Behavior<TextBox> {

    private bool _hasFocus;

    private void AssociatedObject_GotFocus(object sender, Windows.UI.Xaml.RoutedEventArgs e) {
        _hasFocus = true;
        UpdateState(true);
    }

    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e) {
        UpdateState(false);
        this.AssociatedObject.Loaded -= this.AssociatedObject_Loaded;
    }

    private void AssociatedObject_LostFocus(object sender, Windows.UI.Xaml.RoutedEventArgs e) {
        _hasFocus = false;
        UpdateState(true);
    }

    private void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e) {
        UpdateState(false);
    }

    protected override void OnAttached() {
        base.OnAttached();
        this.AssociatedObject.Loaded += this.AssociatedObject_Loaded;
        this.AssociatedObject.GotFocus += this.AssociatedObject_GotFocus;
        this.AssociatedObject.LostFocus += this.AssociatedObject_LostFocus;
        this.AssociatedObject.TextChanged += this.AssociatedObject_TextChanged;
    }

    protected override void OnDetaching() {
        base.OnDetaching();
        this.AssociatedObject.GotFocus -= this.AssociatedObject_GotFocus;
        this.AssociatedObject.LostFocus -= this.AssociatedObject_LostFocus;
        this.AssociatedObject.TextChanged -= this.AssociatedObject_TextChanged;
    }


    private void UpdateState(bool animate) {
        if (_hasFocus || !string.IsNullOrEmpty(this.AssociatedObject.Text)) {
            VisualStateManager.GoToState(this.AssociatedObject, "NotEmpty", animate);
        } else {
            VisualStateManager.GoToState(this.AssociatedObject, "Empty", animate);
        }
    }

}

 

Usage

I dragged and dropped the MoveHeaderOnFocusBehavior from the Assets window in Blend for Visual Studio to every TextBox of the Style 'AnimatedHeaderedTextBoxStyle'. That's all.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App121"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      x:Class="App121.MainPage"
      mc:Ignorable="d"><StackPanel Width="300"
                Margin="0,0,60,0"
                VerticalAlignment="Top"
                HorizontalAlignment="Center"><TextBox Header="Firstname"
                 Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                 IsSpellCheckEnabled="False"
                 InputScope="PersonalFullName"><Interactivity:Interaction.Behaviors><local:MoveHeaderOnFocusBehavior /></Interactivity:Interaction.Behaviors></TextBox><TextBox Header="Lastname"
                 Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                 IsSpellCheckEnabled="False"
                 Margin="0,8"
                 InputScope="PersonalFullName"><Interactivity:Interaction.Behaviors><local:MoveHeaderOnFocusBehavior /></Interactivity:Interaction.Behaviors></TextBox><TextBox Header="E-mail"
                 x:Name="textBoxEmail"
                 Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                 IsSpellCheckEnabled="False"
                 Text="fons.sonnemans@reflectionit.nl"
                 InputScope="EmailNameOrAddress"><Interactivity:Interaction.Behaviors><local:MoveHeaderOnFocusBehavior /></Interactivity:Interaction.Behaviors></TextBox><Button Content="Next"
                Click="Button_Click"
                Margin="0,16,0,0" /><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
              Height="640"
              Margin="0,0,-60,0" /></StackPanel></Page>

 

The code

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

I also received some Twitter reactions pointing to this great Inspirational TextBox styles for Windows Phone & Store blogpost from Deani Hansen. That solution uses Styling and subclassing the TextBox control. As always there are many solutions for the same problem.

Fons

TechDays 2016 video en presentatie

Paging in ASP.NET Core MVC and EntityFramework Core

$
0
0

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<Suppliers>.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<Suppliers>.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<Products>.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

 

New Spacing property in StackPanel

$
0
0

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

New ColumnSpacing and RowSpacing properties for Grid

$
0
0

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

Replace a RepositionThemeTransition with an ImplicitAnimation

$
0
0

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

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


XAML CalculatorBehavior

$
0
0

I have been using Adobe software recently and I noticed you could do simple calculations in textboxes. I used it to export Tile Images in different scale sizes. If the 100% scale of an Image is 150 pixels wide you can enter '150 * 1.5'. It will calculate the width of 225 pixels for the 150% scale size. I loved this feature so I tried to implement it also for my own Xaml apps.

The solution is quite simple. I have created a Behavior called CalculatorBehavior. You just use Blend for Visual Studio to drop it on a TextBox control and you are done.

Demo App

The following video shows you how you can use it to do simpel calculations in a TextBox using the CalculatorBehavior.

CalculatorBehavior Demo

My Solution

My Visual Studio solution contains 3 projects: Windows 10 (UWP), Windows 8.1 and WPF. The WPF project has a NuGet reference to Math Expression Evaluator. This project contains the ExpressionEvaluator class which I use for my calculations. There is no WinRT, PCL (Portable Class Library) or .NET Standard implementation for this project which makes it unusable for my Windows (8.1 and 10/UWP) projects. Luckily the project is Open Sources so I added the ExpressionEvaluator class to these projects.

The Windows 10 project uses the XamlBehaviors project using the NuGet package. The Windows 8.1 project uses the 'Behaviors SDK' but that doesn't include a Behavior<T> class. So i added my own implementation for it in my project. The WPF project has a reference to the 'System.Windows.Interactivity' assembly.

CalculatorBehavior

The CalculatorBehavior code is very simple. The class derives from Behavior<TextBox> which makes it droppable on a TextBox. The LostFocus event of the TextBox will trigger the calculation for which I use the ExpressionEvaluator.

using Microsoft.Xaml.Interactivity;
using SimpleExpressionEvaluator;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace CalculatorUwpDemoApp.Behaviors {

    class CalculatorBehavior : Behavior<TextBox> {

        private static readonly ExpressionEvaluator _evaluator =
            new ExpressionEvaluator();

        protected override void OnAttached() {
            base.OnAttached();
            this.AssociatedObject.LostFocus += Evaluate;
        }

        protected override void OnDetaching() {
            base.OnDetaching();
            this.AssociatedObject.LostFocus -= Evaluate;
        }

        private void Evaluate(object sender, RoutedEventArgs e) {
            try {
                string txt = this.AssociatedObject.Text;
                if (!string.IsNullOrWhiteSpace(txt)) {
                    var result = _evaluator.Evaluate(txt);
                    this.AssociatedObject.Text = result.ToString();
                }
            } catch { }
        }
    }
}

Blend for Visual Studio

Adding the CalculatorBehavior to a TextBox is very easy in Blend for Visual Studio. Select the behavior from the Assets panel and Drag&Drop it on a TextBox. That's all.

Blend Screenshot

The code

I have published my code on GitHub. I hope you like it.

Fons

XAML ScrollSelectedItemIntoViewBehavior

$
0
0

I love C#, XAML and Blend. It is very powerful and lets me create powerful solutions. As a example of it's power I will demonstrate my ScrollSelectedItemIntoViewBehavior. It will let you scroll to a selected item into view of a ListView or GridView without having to write any code.

Demo

The following video (GIF) shows you how you can use it to scroll to the selected product into view of a ListView. This can be done animated or instant.

ScrollSelectedItemIntoViewBehavior Demo

My Solution

My solution is a Behavior which you can apply on any ListView or GridView using Blend for Visual Studio. I have included the Microsoft.Xaml.Behaviors.Uwp.Managed NuGet package to the project. The ScrollSelectedItemIntoViewBehavior class derives from the Behavior<T> class (from the NuGet package) in which T is a ListViewBase. This allows me to use (drop) the behavior on a ListView or GridView. In the OnAttached() and OnDetached() methods I subscribe/unsubscribe to the SelectionChanged event of the AssociatedObject, the ListView or GridView. In the AssociatedObject_SelectionChanged() method the AssociatedObject scrolls to the selected item. For the scrolling I use some code (extension methods) I found on StackOverflow which does the hard work. I only changed some naming of the methods to follow my own code conventions.

public class ScrollSelectedItemIntoViewBehavior : Behavior<ListViewBase> {

    protected override void OnAttached() {
        AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
        base.OnAttached();
    }
    protected override void OnDetaching() {
        AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
        base.OnDetaching();
    }

    private async void AssociatedObject_SelectionChanged(object sender,
                                                    SelectionChangedEventArgs e) {
        var item = e.AddedItems.FirstOrDefault();
        if (item != null) {
            if (IsScrollAnimated) {
                await this.AssociatedObject.ScrollToItemAsync(item);
            } else {
                await this.AssociatedObject.ScrollIntoViewAsync(item);
            }
        }
    }

    /// <summary>
    /// Get or Sets the IsScrollAnimated dependency property.
    /// </summary>
    public bool IsScrollAnimated {
        get { return (bool)GetValue(IsScrollAnimatedProperty); }
        set { SetValue(IsScrollAnimatedProperty, value); }
    }

    /// <summary>
    /// Identifies the IsScrollAnimated dependency property.
    /// This enables animation, styling, binding, etc...
    /// </summary>
    public static readonly DependencyProperty IsScrollAnimatedProperty =
        DependencyProperty.Register(nameof(IsScrollAnimated),
                            typeof(bool),
                            typeof(ScrollSelectedItemIntoViewBehavior),
                            new PropertyMetadata(true));
}

 

Using the Behavior

My sample project also contains some SampleData (products) which I used to populate a ListView. I have dragged the ScrollSelectedItemIntoViewBehavior from the Assets panel and dropped it on the ListView. The behavior also contains an IsScrollAnimated dependency property. The ToggleSwitch is databound to this property.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App47"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      xmlns:Behaviors="using:App47.Behaviors"
      x:Class="App47.MainPage"
      mc:Ignorable="d"><Page.Resources><DataTemplate x:Key="ProductTemplate"><Grid Height="80"
                  Margin="0,4"><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Image Source="{Binding ImageUrl}"
                       Height="80"
                       Width="80" /><StackPanel Grid.Column="1"
                            Margin="8,0,0,0"><TextBlock Text="{Binding Name}"
                               Style="{StaticResource TitleTextBlockStyle}" /><TextBlock Text="{Binding Price}"
                               Style="{StaticResource CaptionTextBlockStyle}"
                               TextWrapping="NoWrap" /></StackPanel></Grid></DataTemplate></Page.Resources><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
          DataContext="{Binding Source={StaticResource SampleDataSource}}"><Grid.ColumnDefinitions><ColumnDefinition Width="380*" /><ColumnDefinition Width="1541*" /></Grid.ColumnDefinitions><StackPanel><Button Content="First"
                    Click="ButtonFirst_Click"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    Margin="4" /><Button Content="Middle"
                    Click="ButtonMiddle_Click"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    Margin="4" /><Button Content="Last"
                    Click="ButtonLast_Click"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    Margin="4" /><ToggleSwitch Header="IsAnimated"
                          Margin="4"
                          IsOn="{x:Bind myBehavior.IsScrollAnimated, Mode=TwoWay}" /></StackPanel><ListView x:Name="listViewProducts"
                  Grid.Column="1"
                  ItemTemplate="{StaticResource ProductTemplate}"
                  ItemsSource="{Binding Products}"><Interactivity:Interaction.Behaviors><Behaviors:ScrollSelectedItemIntoViewBehavior x:Name="myBehavior" /></Interactivity:Interaction.Behaviors></ListView></Grid></Page>

The buttons click eventhandlers only set the SelectedIndex of the ListView. An MVVM solution in which the SelectedIndex is databound to a property of a model would also work.

public sealed partial class MainPage : Page {

    public MainPage() {
        this.InitializeComponent();
    }

    private void ButtonFirst_Click(object sender, RoutedEventArgs e) {
        listViewProducts.SelectedIndex = 0;
    }

    private void ButtonMiddle_Click(object sender, RoutedEventArgs e) {
        listViewProducts.SelectedIndex = (listViewProducts.Items.Count - 1) / 2;
    }

    private void ButtonLast_Click(object sender, RoutedEventArgs e) {
        listViewProducts.SelectedIndex = listViewProducts.Items.Count - 1;

    }
}

The code

I have published my code on GitHub. I hope you like it.

Fons

XAML Storyboard.IsPlaying Attached Property

$
0
0

XAML is very powerful. I have blogged about behaviors a lot. They are great but also have limitations. You can only drag them on Controls, not on Storyboards. To "fix" this I came up with a solution using Attached Properties. The designer support isn't as good with Behaviors but who needs that if we have IntelliSense.

IsPlaying Attached Property

I have created a StoryboardServices class which contains the IsPlaying attached property of the type boolean. In the OnIsPlayingChanged() method I Begin() or Resume() the Storyboard if it is set to True, I Pause() it when it is set to False.

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Animation;

namespace AnimationDemo {
    public class StoryboardServices {

        #region IsPlaying Attached Property

        /// <summary>
        /// Identifies the IsPlaying attachted property. This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty IsPlayingProperty =
            DependencyProperty.RegisterAttached("IsPlaying",
                typeof(bool),
                typeof(StoryboardServices),
                new PropertyMetadata(false, OnIsPlayingChanged));

        /// <summary>
        /// IsPlaying changed handler.
        /// </summary>
        /// <param name="d">FrameworkElement that changed its IsPlaying attached property.</param>
        /// <param name="e">DependencyPropertyChangedEventArgs with the new and old value.</param>
        private static void OnIsPlayingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            var source = d as Storyboard;
            if (source != null) {
                var value = (bool)e.NewValue;
                if (value) {
                    if (source.GetCurrentState() == ClockState.Stopped) {
                        source.Begin();
                    } else {
                        source.Resume();
                    }
                } else {
                    source.Pause();
                }
            }
        }

        /// <summary>
        /// Gets the value of the IsPlaying attached property from the specified FrameworkElement.
        /// </summary>
        public static bool GetIsPlaying(DependencyObject obj) {
            return (bool)obj.GetValue(IsPlayingProperty);
        }


        /// <summary>
        /// Sets the value of the IsPlaying attached property to the specified FrameworkElement.
        /// </summary>
        /// <param name="obj">The object on which to set the IsPlaying attached property.</param>
        /// <param name="value">The property value to set.</param>
        public static void SetIsPlaying(DependencyObject obj, bool value) {
            obj.SetValue(IsPlayingProperty, value);
        }

        #endregion IsPlaying Attached Property

    }
}

Example

In this example I have a Storyboard1 which plays when the checkbox on the page is checked. I databound the IsPlaying attached property to the IsChecked property of the checkbox (see line 10). The animation is really simple but you get the idea.

<Page x:Class="AnimationDemo.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:AnimationDemo"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d"><Page.Resources><Storyboard x:Name="Storyboard1"
             local:StoryboardServices.IsPlaying="{Binding IsChecked, ElementName=checkBoxIsAnimated, Mode=OneWay}"
             RepeatBehavior="Forever"><ColorAnimationUsingKeyFrames
                    Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                   Storyboard.TargetName="myBorder"><EasingColorKeyFrame KeyTime="0:0:1"
                                     Value="Blue" /><EasingColorKeyFrame KeyTime="0:0:2"
                                     Value="Red" /></ColorAnimationUsingKeyFrames></Storyboard></Page.Resources><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"><Grid.ColumnDefinitions><ColumnDefinition Width="1*" /><ColumnDefinition Width="2*" /><ColumnDefinition Width="1*" /></Grid.ColumnDefinitions><CheckBox x:Name="checkBoxIsAnimated"
                  Content="Animate"
                  IsChecked="True"
                  VerticalAlignment="Top"
                  HorizontalAlignment="Center"
                  Grid.Column="1" /><Border x:Name="myBorder"
                Height="100"
                Grid.Column="1"
                Background="Red" /></Grid></Page>

The code

I have published my code on GitHub. I hope you like it.

Fons

XAML Animated Headered TextBox Style

$
0
0

A while ago I saw a beautiful effect in an Android app which I was using. It had textboxes with a placeholder text. This placeholder text moved to the header when you enter the first character in the textbox. This triggered me to do the same in a Windows 10 XAML app (UWP). This is a video of the result.

AnimatedHeaderedTextBoxStyle demo video

AnimatedHeaderedTextBoxStyle

My solution is quite simple. I created a Style for a TextBox which has an extra StateGroup named 'HeaderStates'. There are two States in this group named Empty and NotEmpty. The HeaderContentPresenter is moved using TranslateX and TranslateY setters to the correct position. The states are triggered using TemplatedParent databinding to the Text property. I have used a converter which returns true or false when the text is empty or not. I have also changed the BorderThickness property of the Style. It now has only a bottom line and is not boxed any more.

<Style x:Key="AnimatedHeaderedTextBoxStyle"
        TargetType="TextBox"><Setter Property="MinWidth"
            Value="{ThemeResource TextControlThemeMinWidth}" /><Setter Property="MinHeight"
            Value="{ThemeResource TextControlThemeMinHeight}" /><Setter Property="Foreground"
            Value="{ThemeResource TextControlForeground}" /><Setter Property="Background"
            Value="{ThemeResource TextControlBackground}" /><Setter Property="BorderBrush"
            Value="{ThemeResource TextControlBorderBrush}" /><Setter Property="SelectionHighlightColor"
            Value="{ThemeResource TextControlSelectionHighlightColor}" /><Setter Property="BorderThickness"
            Value="0,0,0,2" /><Setter Property="FontFamily"
            Value="{ThemeResource ContentControlThemeFontFamily}" /><Setter Property="FontSize"
            Value="{ThemeResource ControlContentThemeFontSize}" /><Setter Property="ScrollViewer.HorizontalScrollMode"
            Value="Auto" /><Setter Property="ScrollViewer.VerticalScrollMode"
            Value="Auto" /><Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
            Value="Hidden" /><Setter Property="ScrollViewer.VerticalScrollBarVisibility"
            Value="Hidden" /><Setter Property="ScrollViewer.IsDeferredScrollingEnabled"
            Value="False" /><Setter Property="Padding"
            Value="{ThemeResource TextControlThemePadding}" /><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="TextBox"><Grid><Grid.Resources><Style x:Name="DeleteButtonStyle"
                                TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid x:Name="ButtonLayoutGrid"
                                                BorderBrush="{ThemeResource TextControlButtonBorderBrush}"
                                                BorderThickness="{TemplateBinding BorderThickness}"
                                                Background="{ThemeResource TextControlButtonBackground}"><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Normal" /><VisualState x:Name="PointerOver"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBackgroundPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBorderBrushPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                                            Storyboard.TargetName="GlyphElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonForegroundPointerOver}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Pressed"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBackgroundPressed}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                                            Storyboard.TargetName="ButtonLayoutGrid"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonBorderBrushPressed}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                                            Storyboard.TargetName="GlyphElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                                        Value="{ThemeResource TextControlButtonForegroundPressed}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Disabled"><Storyboard><DoubleAnimation Duration="0"
                                                                                To="0"
                                                                                Storyboard.TargetProperty="Opacity"
                                                                                Storyboard.TargetName="ButtonLayoutGrid" /></Storyboard></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><TextBlock x:Name="GlyphElement"
                                                        AutomationProperties.AccessibilityView="Raw"
                                                        Foreground="{ThemeResource TextControlButtonForeground}"
                                                        FontStyle="Normal"
                                                        FontSize="12"
                                                        FontFamily="{ThemeResource SymbolThemeFontFamily}"
                                                        HorizontalAlignment="Center"
                                                        Text="&#xE10A;"
                                                        VerticalAlignment="Center" /></Grid></ControlTemplate></Setter.Value></Setter></Style></Grid.Resources><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="*" /></Grid.RowDefinitions><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Disabled"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="HeaderContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlHeaderForegroundDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBackgroundDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBorderBrushDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlForegroundDisabled}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="PlaceholderTextContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlPlaceholderForegroundDisabled}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Normal" /><VisualState x:Name="PointerOver"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBorderBrushPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBackgroundPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="PlaceholderTextContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlPlaceholderForegroundPointerOver}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlForegroundPointerOver}" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Focused"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="PlaceholderTextContentPresenter"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlPlaceholderForegroundFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBackgroundFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
                                                                    Storyboard.TargetName="BorderElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlBorderBrushFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="{ThemeResource TextControlForegroundFocused}" /></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="RequestedTheme"
                                                                    Storyboard.TargetName="ContentElement"><DiscreteObjectKeyFrame KeyTime="0"
                                                                Value="Light" /></ObjectAnimationUsingKeyFrames></Storyboard></VisualState></VisualStateGroup><VisualStateGroup x:Name="ButtonStates"><VisualState x:Name="ButtonVisible"><Storyboard><ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility"
                                                                    Storyboard.TargetName="DeleteButton"><DiscreteObjectKeyFrame KeyTime="0"><DiscreteObjectKeyFrame.Value><Visibility>Visible</Visibility></DiscreteObjectKeyFrame.Value></DiscreteObjectKeyFrame></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="ButtonCollapsed" /></VisualStateGroup><VisualStateGroup x:Name="HeaderStates"><VisualStateGroup.Transitions><VisualTransition GeneratedDuration="0:0:0.35"><VisualTransition.GeneratedEasingFunction><CircleEase EasingMode="EaseInOut" /></VisualTransition.GeneratedEasingFunction></VisualTransition></VisualStateGroup.Transitions><VisualState x:Name="NotEmpty"><VisualState.Setters><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
                                            Value="0" /><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateX)"
                                            Value="0" /></VisualState.Setters><VisualState.StateTriggers><StateTrigger IsActive="{Binding Text, Converter={StaticResource IsNotEmptyConverter}, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /></VisualState.StateTriggers></VisualState><VisualState x:Name="Empty"><VisualState.Setters><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
                                            Value="31" /><Setter Target="HeaderContentPresenter.(UIElement.RenderTransform).(CompositeTransform.TranslateX)"
                                            Value="11" /></VisualState.Setters><VisualState.StateTriggers><StateTrigger IsActive="{Binding Text, Converter={StaticResource IsEmptyConverter}, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /></VisualState.StateTriggers></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><Border x:Name="BorderElement"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}"
                            Grid.ColumnSpan="2"
                            Grid.Row="1"
                            Grid.RowSpan="1" /><ContentPresenter x:Name="HeaderContentPresenter"
                                        Grid.ColumnSpan="2"
                                        ContentTemplate="{TemplateBinding HeaderTemplate}"
                                        Content="{TemplateBinding Header}"
                                        Foreground="{ThemeResource TextControlHeaderForeground}"
                                        FontWeight="Normal"
                                        Margin="0,0,0,8"
                                        Grid.Row="0"
                                        Visibility="Collapsed"
                                        x:DeferLoadStrategy="Lazy"
                                        RenderTransformOrigin="0.5,0.5"><ContentPresenter.RenderTransform><CompositeTransform /></ContentPresenter.RenderTransform></ContentPresenter><ScrollViewer x:Name="ContentElement"
                                    AutomationProperties.AccessibilityView="Raw"
                                    HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
                                    HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
                                    IsTabStop="False"
                                    IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
                                    IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
                                    IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
                                    Margin="{TemplateBinding BorderThickness}"
                                    Padding="{TemplateBinding Padding}"
                                    Grid.Row="1"
                                    VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
                                    VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
                                    ZoomMode="Disabled" /><ContentControl x:Name="PlaceholderTextContentPresenter"
                                    Grid.ColumnSpan="2"
                                    Content="{TemplateBinding PlaceholderText}"
                                    Foreground="{ThemeResource TextControlPlaceholderForeground}"
                                    IsHitTestVisible="False"
                                    IsTabStop="False"
                                    Margin="{TemplateBinding BorderThickness}"
                                    Padding="{TemplateBinding Padding}"
                                    Grid.Row="1" /><Button x:Name="DeleteButton"
                            AutomationProperties.AccessibilityView="Raw"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Grid.Column="1"
                            FontSize="{TemplateBinding FontSize}"
                            IsTabStop="False"
                            Margin="{ThemeResource HelperButtonThemePadding}"
                            MinWidth="34"
                            Grid.Row="1"
                            Style="{StaticResource DeleteButtonStyle}"
                            Visibility="Collapsed"
                            VerticalAlignment="Stretch" /></Grid></ControlTemplate></Setter.Value></Setter></Style>

The converter is very simple. It has an extra IsInvertered property to invert a True to a False.

public class EmptyStringToTrueConverter : IValueConverter {
    public bool IsInverted { get; set; }

    public object Convert(object value, Type targetType, object parameter, string language) {
        var txt = (string)value;
        return string.IsNullOrEmpty(txt) ^ IsInverted;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language) {
        return DependencyProperty.UnsetValue;
    }
}

The converteres are registered using the following XAML.

<local:EmptyStringToTrueConverter x:Key="IsNotEmptyConverter" IsInverted="True" /><local:EmptyStringToTrueConverter x:Key="IsEmptyConverter" />

Usage

My example page which I used in the video is shown below. All TextBoxes have the Style property set to AnimatedHeaderedTextBoxStyle and off course a Header property. I have set the IsSpellCheckEnable property to False because I don't want spellchecking on names and email addresses.  

<Page x:Class="App121.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App121"
      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}"><StackPanel Width="300"
                    Margin="0,12"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Center"><TextBox Header="Firstname"
                     Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                     IsSpellCheckEnabled="False"
                     InputScope="PersonalFullName"/><TextBox Header="Lastname"
                     Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                     IsSpellCheckEnabled="False"
                     Margin="0,8"
                     InputScope="PersonalFullName" /><TextBox Header="E-mail"
                     Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                     IsSpellCheckEnabled="False"
                     InputScope="EmailNameOrAddress" /><Button Content="Next"
                    Margin="0,16,0,0" /></StackPanel></Grid></Page>

I have placed the AnimatedHeaderedTextBoxStyle, the registration of the converters and a DeleteButtonStyle into ResourceDictionary named AnimatedHeaderedTextBoxStyle.xaml. The DeleteButtonStyle was generated by Blend when I created the Style by 'Editing a Copy...' of the Template for an existing TextBox.

Edit a Copy

In have registered the ResourceDictionary from the App.xaml which makes it available for in all pages and controls in this application.

<Application
    x:Class="App121.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App121"
    RequestedTheme="Light"><Application.Resources><ResourceDictionary><ResourceDictionary.MergedDictionaries><ResourceDictionary Source="AnimatedHeaderedTextBoxStyle.xaml" /></ResourceDictionary.MergedDictionaries></ResourceDictionary></Application.Resources></Application>

The easiest way to add this Resource Dictionary as a MergedDictionaries in the App.xaml is to use Blend. Open the Resources Window and right click the App.xaml file. Then select 'Link to Resource Dictionary' and check the AnimatedHeaderedTextBoxStyle.xaml file.

Link to ResourceDictionary

The code

I have published my code on GitHub. I hope you like it.

Fons

XAML Animated Headered TextBox Style (Part 2)

$
0
0

Last week I wrote a blog item about the XAML Animated Headered TextBox Style. I got a lot of reactions on Twitter. Pieter Otten challenged me into adjusting my solution to make it conform the Material Design guidelines. In Android the header (Label) of the TextBox (Text Field) animates on Focus not when you enter a value. I tried to implement this by adjusting my style but that didn't work. I had to introduce an extra behavior to get it to work. See the result in this video.

AnimatedHeaderedTextBoxStyle demo video which works on Focus

MoveHeaderOnFocusBehavior

The MoveHeaderOnFocusBehavior is used to set the Visual States (Empty and NotEmpty) of the associated TextBox when it is loaded, gets the focus, loses the focus or when the text changes. This Behavior was build by subclassing the Behavior<T> class from the Microsoft.Xaml.Behaviors.Uwp.Managed NuGet package.

public class MoveHeaderOnFocusBehavior : Behavior<TextBox> {

    private bool _hasFocus;

    private void AssociatedObject_GotFocus(object sender, Windows.UI.Xaml.RoutedEventArgs e) {
        _hasFocus = true;
        UpdateState(true);
    }

    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e) {
        UpdateState(false);
        this.AssociatedObject.Loaded -= this.AssociatedObject_Loaded;
    }

    private void AssociatedObject_LostFocus(object sender, Windows.UI.Xaml.RoutedEventArgs e) {
        _hasFocus = false;
        UpdateState(true);
    }

    private void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e) {
        UpdateState(false);
    }

    protected override void OnAttached() {
        base.OnAttached();
        this.AssociatedObject.Loaded += this.AssociatedObject_Loaded;
        this.AssociatedObject.GotFocus += this.AssociatedObject_GotFocus;
        this.AssociatedObject.LostFocus += this.AssociatedObject_LostFocus;
        this.AssociatedObject.TextChanged += this.AssociatedObject_TextChanged;
    }

    protected override void OnDetaching() {
        base.OnDetaching();
        this.AssociatedObject.GotFocus -= this.AssociatedObject_GotFocus;
        this.AssociatedObject.LostFocus -= this.AssociatedObject_LostFocus;
        this.AssociatedObject.TextChanged -= this.AssociatedObject_TextChanged;
    }


    private void UpdateState(bool animate) {
        if (_hasFocus || !string.IsNullOrEmpty(this.AssociatedObject.Text)) {
            VisualStateManager.GoToState(this.AssociatedObject, "NotEmpty", animate);
        } else {
            VisualStateManager.GoToState(this.AssociatedObject, "Empty", animate);
        }
    }

}

 

Usage

I dragged and dropped the MoveHeaderOnFocusBehavior from the Assets window in Blend for Visual Studio to every TextBox of the Style 'AnimatedHeaderedTextBoxStyle'. That's all.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App121"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      x:Class="App121.MainPage"
      mc:Ignorable="d"><StackPanel Width="300"
                Margin="0,0,60,0"
                VerticalAlignment="Top"
                HorizontalAlignment="Center"><TextBox Header="Firstname"
                 Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                 IsSpellCheckEnabled="False"
                 InputScope="PersonalFullName"><Interactivity:Interaction.Behaviors><local:MoveHeaderOnFocusBehavior /></Interactivity:Interaction.Behaviors></TextBox><TextBox Header="Lastname"
                 Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                 IsSpellCheckEnabled="False"
                 Margin="0,8"
                 InputScope="PersonalFullName"><Interactivity:Interaction.Behaviors><local:MoveHeaderOnFocusBehavior /></Interactivity:Interaction.Behaviors></TextBox><TextBox Header="E-mail"
                 x:Name="textBoxEmail"
                 Style="{StaticResource AnimatedHeaderedTextBoxStyle}"
                 IsSpellCheckEnabled="False"
                 Text="fons.sonnemans@reflectionit.nl"
                 InputScope="EmailNameOrAddress"><Interactivity:Interaction.Behaviors><local:MoveHeaderOnFocusBehavior /></Interactivity:Interaction.Behaviors></TextBox><Button Content="Next"
                Click="Button_Click"
                Margin="0,16,0,0" /><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
              Height="640"
              Margin="0,0,-60,0" /></StackPanel></Page>

 

The code

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

I also received some Twitter reactions pointing to this great Inspirational TextBox styles for Windows Phone & Store blogpost from Deani Hansen. That solution uses Styling and subclassing the TextBox control. As always there are many solutions for the same problem.

Fons

Paging in ASP.NET Core MVC and EntityFramework Core

$
0
0

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

 

New Spacing property in StackPanel

$
0
0

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


New ColumnSpacing and RowSpacing properties for Grid

$
0
0

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

Replace a RepositionThemeTransition with an ImplicitAnimation

$
0
0

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

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

XAML CalculatorBehavior

$
0
0

I have been using Adobe software recently and I noticed you could do simple calculations in textboxes. I used it to export Tile Images in different scale sizes. If the 100% scale of an Image is 150 pixels wide you can enter '150 * 1.5'. It will calculate the width of 225 pixels for the 150% scale size. I loved this feature so I tried to implement it also for my own Xaml apps.

The solution is quite simple. I have created a Behavior called CalculatorBehavior. You just use Blend for Visual Studio to drop it on a TextBox control and you are done.

Demo App

The following video shows you how you can use it to do simpel calculations in a TextBox using the CalculatorBehavior.

CalculatorBehavior Demo

My Solution

My Visual Studio solution contains 3 projects: Windows 10 (UWP), Windows 8.1 and WPF. The WPF project has a NuGet reference to Math Expression Evaluator. This project contains the ExpressionEvaluator class which I use for my calculations. There is no WinRT, PCL (Portable Class Library) or .NET Standard implementation for this project which makes it unusable for my Windows (8.1 and 10/UWP) projects. Luckily the project is Open Sources so I added the ExpressionEvaluator class to these projects.

The Windows 10 project uses the XamlBehaviors project using the NuGet package. The Windows 8.1 project uses the 'Behaviors SDK' but that doesn't include a Behavior<T> class. So i added my own implementation for it in my project. The WPF project has a reference to the 'System.Windows.Interactivity' assembly.

CalculatorBehavior

The CalculatorBehavior code is very simple. The class derives from Behavior<TextBox> which makes it droppable on a TextBox. The LostFocus event of the TextBox will trigger the calculation for which I use the ExpressionEvaluator.

using Microsoft.Xaml.Interactivity;
using SimpleExpressionEvaluator;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace CalculatorUwpDemoApp.Behaviors {

    class CalculatorBehavior : Behavior<TextBox> {

        private static readonly ExpressionEvaluator _evaluator =
            new ExpressionEvaluator();

        protected override void OnAttached() {
            base.OnAttached();
            this.AssociatedObject.LostFocus += Evaluate;
        }

        protected override void OnDetaching() {
            base.OnDetaching();
            this.AssociatedObject.LostFocus -= Evaluate;
        }

        private void Evaluate(object sender, RoutedEventArgs e) {
            try {
                string txt = this.AssociatedObject.Text;
                if (!string.IsNullOrWhiteSpace(txt)) {
                    var result = _evaluator.Evaluate(txt);
                    this.AssociatedObject.Text = result.ToString();
                }
            } catch { }
        }
    }
}

Blend for Visual Studio

Adding the CalculatorBehavior to a TextBox is very easy in Blend for Visual Studio. Select the behavior from the Assets panel and Drag&Drop it on a TextBox. That's all.

Blend Screenshot

The code

I have published my code on GitHub. I hope you like it.

Fons

XAML ScrollSelectedItemIntoViewBehavior

$
0
0

I love C#, XAML and Blend. It is very powerful and lets me create powerful solutions. As a example of it's power I will demonstrate my ScrollSelectedItemIntoViewBehavior. It will let you scroll to a selected item into view of a ListView or GridView without having to write any code.

Demo

The following video (GIF) shows you how you can use it to scroll to the selected product into view of a ListView. This can be done animated or instant.

ScrollSelectedItemIntoViewBehavior Demo

My Solution

My solution is a Behavior which you can apply on any ListView or GridView using Blend for Visual Studio. I have included the Microsoft.Xaml.Behaviors.Uwp.Managed NuGet package to the project. The ScrollSelectedItemIntoViewBehavior class derives from the Behavior<T> class (from the NuGet package) in which T is a ListViewBase. This allows me to use (drop) the behavior on a ListView or GridView. In the OnAttached() and OnDetached() methods I subscribe/unsubscribe to the SelectionChanged event of the AssociatedObject, the ListView or GridView. In the AssociatedObject_SelectionChanged() method the AssociatedObject scrolls to the selected item. For the scrolling I use some code (extension methods) I found on StackOverflow which does the hard work. I only changed some naming of the methods to follow my own code conventions.

public class ScrollSelectedItemIntoViewBehavior : Behavior<ListViewBase> {

    protected override void OnAttached() {
        AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
        base.OnAttached();
    }
    protected override void OnDetaching() {
        AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
        base.OnDetaching();
    }

    private async void AssociatedObject_SelectionChanged(object sender,
                                                    SelectionChangedEventArgs e) {
        var item = e.AddedItems.FirstOrDefault();
        if (item != null) {
            if (IsScrollAnimated) {
                await this.AssociatedObject.ScrollToItemAsync(item);
            } else {
                await this.AssociatedObject.ScrollIntoViewAsync(item);
            }
        }
    }

    /// <summary>
    /// Get or Sets the IsScrollAnimated dependency property.
    /// </summary>
    public bool IsScrollAnimated {
        get { return (bool)GetValue(IsScrollAnimatedProperty); }
        set { SetValue(IsScrollAnimatedProperty, value); }
    }

    /// <summary>
    /// Identifies the IsScrollAnimated dependency property.
    /// This enables animation, styling, binding, etc...
    /// </summary>
    public static readonly DependencyProperty IsScrollAnimatedProperty =
        DependencyProperty.Register(nameof(IsScrollAnimated),
                            typeof(bool),
                            typeof(ScrollSelectedItemIntoViewBehavior),
                            new PropertyMetadata(true));
}

 

Using the Behavior

My sample project also contains some SampleData (products) which I used to populate a ListView. I have dragged the ScrollSelectedItemIntoViewBehavior from the Assets panel and dropped it on the ListView. The behavior also contains an IsScrollAnimated dependency property. The ToggleSwitch is databound to this property.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App47"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      xmlns:Behaviors="using:App47.Behaviors"
      x:Class="App47.MainPage"
      mc:Ignorable="d"><Page.Resources><DataTemplate x:Key="ProductTemplate"><Grid Height="80"
                  Margin="0,4"><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><Image Source="{Binding ImageUrl}"
                       Height="80"
                       Width="80" /><StackPanel Grid.Column="1"
                            Margin="8,0,0,0"><TextBlock Text="{Binding Name}"
                               Style="{StaticResource TitleTextBlockStyle}" /><TextBlock Text="{Binding Price}"
                               Style="{StaticResource CaptionTextBlockStyle}"
                               TextWrapping="NoWrap" /></StackPanel></Grid></DataTemplate></Page.Resources><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
          DataContext="{Binding Source={StaticResource SampleDataSource}}"><Grid.ColumnDefinitions><ColumnDefinition Width="380*" /><ColumnDefinition Width="1541*" /></Grid.ColumnDefinitions><StackPanel><Button Content="First"
                    Click="ButtonFirst_Click"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    Margin="4" /><Button Content="Middle"
                    Click="ButtonMiddle_Click"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    Margin="4" /><Button Content="Last"
                    Click="ButtonLast_Click"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
                    Margin="4" /><ToggleSwitch Header="IsAnimated"
                          Margin="4"
                          IsOn="{x:Bind myBehavior.IsScrollAnimated, Mode=TwoWay}" /></StackPanel><ListView x:Name="listViewProducts"
                  Grid.Column="1"
                  ItemTemplate="{StaticResource ProductTemplate}"
                  ItemsSource="{Binding Products}"><Interactivity:Interaction.Behaviors><Behaviors:ScrollSelectedItemIntoViewBehavior x:Name="myBehavior" /></Interactivity:Interaction.Behaviors></ListView></Grid></Page>

The buttons click eventhandlers only set the SelectedIndex of the ListView. An MVVM solution in which the SelectedIndex is databound to a property of a model would also work.

public sealed partial class MainPage : Page {

    public MainPage() {
        this.InitializeComponent();
    }

    private void ButtonFirst_Click(object sender, RoutedEventArgs e) {
        listViewProducts.SelectedIndex = 0;
    }

    private void ButtonMiddle_Click(object sender, RoutedEventArgs e) {
        listViewProducts.SelectedIndex = (listViewProducts.Items.Count - 1) / 2;
    }

    private void ButtonLast_Click(object sender, RoutedEventArgs e) {
        listViewProducts.SelectedIndex = listViewProducts.Items.Count - 1;

    }
}

The code

I have published my code on GitHub. I hope you like it.

Fons

Viewing all 1291 articles
Browse latest View live