Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

Advanced Features

Set UseDragSort="True" on a section to put a drag handle (☰) on every one of its rows. Dragging a handle lifts the row under the finger, draws an insertion line at the drop position, and auto-scrolls when the drag reaches the top or bottom edge of the table. Touches anywhere other than a handle still scroll the table normally, and rows only ever reorder within their own section.

<tv:TableView ItemDroppedCommand="{Binding ItemDroppedCommand}">
<tv:TableRoot>
<tv:TableSection Title="Drag to Reorder" UseDragSort="True">
<tv:LabelCell Title="First" ValueText="1" />
<tv:LabelCell Title="Second" ValueText="2" />
<tv:LabelCell Title="Third" ValueText="3" />
</tv:TableSection>
</tv:TableRoot>
</tv:TableView>

The ItemDroppedCommand receives an ItemDroppedEventArgs with:

Property Type Description
Section TvTableSection The section containing the cell
Cell CellBase The cell that was moved
Item object? The moved cell’s binding context
FromIndex int Original position among the section’s rendered rows
ToIndex int New position among the section’s rendered rows

Cells declared in XAML are reordered by the control itself, so the handler is purely a notification.

Rows generated from a section’s ItemsSource/ItemTemplate are not reordered for you — their order lives in your collection. Use Item to identify the row that moved and ToIndex for where it landed:

<tv:TableSection Title="Players" UseDragSort="True" ItemsSource="{Binding Players}">
<tv:TableSection.ItemTemplate>
<DataTemplate>
<tv:LabelCell Title="{Binding Name}" ValueText="{Binding Position}" />
</DataTemplate>
</tv:TableSection.ItemTemplate>
</tv:TableSection>
void OnItemDropped(ItemDroppedEventArgs args)
{
if (args.Item is Player player)
{
var from = this.Players.IndexOf(player);
if (from >= 0 && from != args.ToIndex)
this.Players.Move(from, args.ToIndex);
}
}

You can also use the event-based approach:

tableView.ItemDropped += (sender, args) =>
{
Console.WriteLine($"Moved from {args.FromIndex} to {args.ToIndex}");
};

Trigger scrolling from your view model using bindable properties:

<tv:TableView ScrollToTop="{Binding ShouldScrollTop}"
ScrollToBottom="{Binding ShouldScrollBottom}" />

Set the bound property to true to trigger the scroll. The control resets it to false after scrolling.

Use the async methods for programmatic scroll control:

// Scroll to top with animation
await tableView.ScrollToTopAsync(animated: true);
// Scroll to bottom with animation
await tableView.ScrollToBottomAsync(animated: true);

Fires when the structure of the TableView changes (sections added/removed, cells added/removed):

tableView.ModelChanged += (sender, args) =>
{
Console.WriteLine("TableView structure changed");
};

Fires when a property changes on any cell within the TableView:

tableView.CellPropertyChanged += (sender, args) =>
{
Console.WriteLine($"Cell property changed: {args.PropertyName}");
};

Here is a comprehensive example showing multiple cell types, sections, styling, and MVVM bindings:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:tv="http://shiny.net/maui/tableview"
x:Class="MyApp.SettingsPage"
Title="Settings">
<tv:TableView CellTitleFontSize="16"
CellValueTextColor="#007AFF"
CellSelectedColor="#E8E8ED"
CellAccentColor="#34C759"
HeaderFontSize="13"
HeaderTextColor="#6D6D72"
FooterTextColor="#8E8E93">
<tv:TableRoot>
<!-- Account Section -->
<tv:TableSection Title="ACCOUNT">
<tv:LabelCell Title="Name"
ValueText="{Binding UserName}"
IconSource="person.png" />
<tv:LabelCell Title="Email"
ValueText="{Binding UserEmail}"
IconSource="email.png" />
<tv:CommandCell Title="Edit Profile"
Command="{Binding EditProfileCommand}"
ShowArrow="True"
IconSource="edit.png" />
</tv:TableSection>
<!-- Preferences Section -->
<tv:TableSection Title="PREFERENCES"
FooterText="Notifications may use cellular data">
<tv:SwitchCell Title="Push Notifications"
On="{Binding PushEnabled, Mode=TwoWay}"
IconSource="bell.png" />
<tv:SwitchCell Title="Email Alerts"
On="{Binding EmailAlerts, Mode=TwoWay}"
IconSource="mail.png" />
<tv:TextPickerCell Title="Language"
ItemsSource="{Binding Languages}"
SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}"
IconSource="globe.png" />
</tv:TableSection>
<!-- Appearance Section -->
<tv:TableSection Title="APPEARANCE"
tv:RadioCell.SelectedValue="{Binding Theme, Mode=TwoWay}">
<tv:RadioCell Title="Light" Value="Light" />
<tv:RadioCell Title="Dark" Value="Dark" />
<tv:RadioCell Title="System" Value="System" />
</tv:TableSection>
<!-- About Section -->
<tv:TableSection Title="ABOUT">
<tv:LabelCell Title="Version" ValueText="1.0.0" />
<tv:CommandCell Title="Privacy Policy"
Command="{Binding PrivacyCommand}"
ShowArrow="True" />
<tv:CommandCell Title="Terms of Service"
Command="{Binding TermsCommand}"
ShowArrow="True" />
</tv:TableSection>
<!-- Actions -->
<tv:TableSection>
<tv:ButtonCell Title="Sign Out"
Command="{Binding SignOutCommand}"
ButtonTextColor="Red" />
</tv:TableSection>
</tv:TableRoot>
</tv:TableView>
</ContentPage>
public class SettingsViewModel : INotifyPropertyChanged
{
private bool _pushEnabled = true;
private bool _emailAlerts;
private string _theme = "System";
private object? _selectedLanguage;
public string UserName => "Jane Doe";
public string UserEmail => "jane@example.com";
public bool PushEnabled
{
get => _pushEnabled;
set => SetProperty(ref _pushEnabled, value);
}
public bool EmailAlerts
{
get => _emailAlerts;
set => SetProperty(ref _emailAlerts, value);
}
public string Theme
{
get => _theme;
set => SetProperty(ref _theme, value);
}
public List<string> Languages { get; } = ["English", "Spanish", "French", "German"];
public object? SelectedLanguage
{
get => _selectedLanguage;
set => SetProperty(ref _selectedLanguage, value);
}
public ICommand EditProfileCommand { get; }
public ICommand PrivacyCommand { get; }
public ICommand TermsCommand { get; }
public ICommand SignOutCommand { get; }
public SettingsViewModel()
{
EditProfileCommand = new Command(async () =>
await Shell.Current.GoToAsync("editprofile"));
PrivacyCommand = new Command(async () =>
await Launcher.OpenAsync("https://example.com/privacy"));
TermsCommand = new Command(async () =>
await Launcher.OpenAsync("https://example.com/terms"));
SignOutCommand = new Command(async () =>
{
bool confirm = await Application.Current!.Windows[0].Page!
.DisplayAlert("Sign Out", "Are you sure?", "Sign Out", "Cancel");
if (confirm) { /* sign out logic */ }
});
}
public event PropertyChangedEventHandler? PropertyChanged;
private bool SetProperty<T>(ref T field, T value,
[System.Runtime.CompilerServices.CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
return true;
}
}