I'm trying to use a VerticalStackLayout for the UI to adjust automatically (expands/collapses) when setting a control to visible/invisible. I have not figured out a way how to do that in a grid, if that is even possible.
The problem with the VerticalStackLayout is that the CollectionView inside of it does not scroll. Another requirement is that the page should not scroll as a whole. Only part of it should scroll. In this case the CollectionView.
With a grid around, it works fine. The collection view scrolls, while the rest of the UI stays fixed.
<ContentPage>
<Grid RowDefinitions="50,*">
<Button Grid.Row="0" />
<CollectionView />
</Grid>
</ContentPage>
The issue with this is, when the button is set to invisible, then there is an empty space because the rows are fix.
Something like this <Grid RowDefinitions="*,*"> also doesn't work, because this just has effect on how the rows are distributed but not if they are collapsing or not.
The next construct works fine for the collapsing part. When the button is invisible, the space is automatically filled with the following elements. But the collection view, does not scroll, thus not all the items in the collection view are visible.
<ContentPage>
<VerticalStackLayout>
<Button />
<CollectionView />
</VerticalStackLayout>
</ContentPage>
How to combine the two features, scrolling and collapsing?
I've also tried a FlexLayout
<FlexLayout Direction="Column"
AlignItems="Center"
JustifyContent="Start">
<Button />
<CollectionView />
</FlexLayout>
But this seems to always use the whole page and thus squeeze the elements if they don't fit. If I set the CollectionView to HeightRequest="100", then I get the desired outcome, with the exception that the CollectionView doesn't go all the way to the bottom anymore and is cut of in the middle of the screen.
And something like HeightRequest="0.9*" for percentage, as suggested in some articles, doesn't seem to work for .NET MAUI.
I've studied the documentation here
https://learn.microsoft.com/en-us/dotnet/maui/user-interface/layouts/flexlayout but I have not found an answer to my question.
Update
CollectionView is itself a scrollView. So if we include a scrollView inside another scrollView.That may lead to undesired behavior and complexity for user to use it properly.
You can reorganize the hierarchy of your code and use headers or supplementary views.
Here, we can add CollectionView.Header to the CollectionView to achieve this .You can refer to the following code,which works properly on my side.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MonkeyBusiness.View.VerticalStackLayoutPage"
xmlns:viewmodel="clr-namespace:MonkeyBusiness.ViewModel"
x:DataType="viewmodel:VerticalStackLayoutViewModel"
xmlns:model="clr-namespace:MonkeyBusiness.Model">
<CollectionView ItemsSource="{Binding Monkeys}">
<CollectionView.Header>
<StackLayout BackgroundColor="LightGray">
<Label x:Name="lblCollapsable"
Text="Click button to collapse this text.
How can this view be adjusted to support scrolling only for the collection view and not for the whole page?"
Margin="20,20,0,0" HorizontalOptions="Fill" HeightRequest="80" />
<Button Text="Collapse content" x:Name="btnCollapse" Clicked="btnCollapse_Clicked" HorizontalOptions="Fill" HeightRequest="60" Margin="6" />
</StackLayout>
</CollectionView.Header>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Monkey">
<Grid Padding="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image Grid.RowSpan="2"
Source="{Binding ImageUrl}"
Aspect="AspectFill"
HeightRequest="60"
WidthRequest="60" />
<Label Grid.Column="1"
Text="{Binding Name}"
FontAttributes="Bold" />
<Label Grid.Row="1"
Grid.Column="1"
Text="{Binding Location}"
FontAttributes="Italic"
VerticalOptions="End" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
If so, could you share your xaml, for me to reproduce?
You can refer to the following code I used:
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
int count = 0;
public MainPage()
{
InitializeComponent();
BindingContext = new MyViewModel();
}
private void testbtn_Clicked(object sender, EventArgs e)
{
testbtn.IsVisible = !testbtn.IsVisible;
}
}
MyViewModel.cs
class MyViewModel: INotifyPropertyChanged
{
readonly IList<Monkey> source;
public ObservableCollection<Monkey> Monkeys { get; private set; }
public MyViewModel()
{
source = new List<Monkey>();
CreateMonkeyCollection();
}
void CreateMonkeyCollection()
{
source.Add(new Monkey
{
Name = "Baboon",
Location = "Africa & Asia",
Details = "Baboons are African and Arabian Old World monkeys belonging to the genus Papio, part of the subfamily Cercopithecinae.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Papio_anubis_%28Serengeti%2C_2009%29.jpg/200px-Papio_anubis_%28Serengeti%2C_2009%29.jpg"
});
source.Add(new Monkey
{
Name = "Capuchin Monkey",
Location = "Central & South America",
Details = "The capuchin monkeys are New World monkeys of the subfamily Cebinae. Prior to 2011, the subfamily contained only a single genus, Cebus.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Capuchin_Costa_Rica.jpg/200px-Capuchin_Costa_Rica.jpg"
});
source.Add(new Monkey
{
Name = "Blue Monkey",
Location = "Central and East Africa",
Details = "The blue monkey or diademed monkey is a species of Old World monkey native to Central and East Africa, ranging from the upper Congo River basin east to the East African Rift and south to northern Angola and Zambia",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/BlueMonkey.jpg/220px-BlueMonkey.jpg"
});
source.Add(new Monkey
{
Name = "Squirrel Monkey",
Location = "Central & South America",
Details = "The squirrel monkeys are the New World monkeys of the genus Saimiri. They are the only genus in the subfamily Saimirinae. The name of the genus Saimiri is of Tupi origin, and was also used as an English name by early researchers.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Saimiri_sciureus-1_Luc_Viatour.jpg/220px-Saimiri_sciureus-1_Luc_Viatour.jpg"
});
source.Add(new Monkey
{
Name = "Golden Lion Tamarin",
Location = "Brazil",
Details = "The golden lion tamarin also known as the golden marmoset, is a small New World monkey of the family Callitrichidae.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/Golden_lion_tamarin_portrait3.jpg/220px-Golden_lion_tamarin_portrait3.jpg"
});
source.Add(new Monkey
{
Name = "Howler Monkey",
Location = "South America",
Details = "Howler monkeys are among the largest of the New World monkeys. Fifteen species are currently recognised. Previously classified in the family Cebidae, they are now placed in the family Atelidae.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Alouatta_guariba.jpg/200px-Alouatta_guariba.jpg"
});
source.Add(new Monkey
{
Name = "Japanese Macaque",
Location = "Japan",
Details = "The Japanese macaque, is a terrestrial Old World monkey species native to Japan. They are also sometimes known as the snow monkey because they live in areas where snow covers the ground for months each",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Macaca_fuscata_fuscata1.jpg/220px-Macaca_fuscata_fuscata1.jpg"
});
source.Add(new Monkey
{
Name = "Mandrill",
Location = "Southern Cameroon, Gabon, Equatorial Guinea, and Congo",
Details = "The mandrill is a primate of the Old World monkey family, closely related to the baboons and even more closely to the drill. It is found in southern Cameroon, Gabon, Equatorial Guinea, and Congo.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Mandrill_at_san_francisco_zoo.jpg/220px-Mandrill_at_san_francisco_zoo.jpg"
});
source.Add(new Monkey
{
Name = "Proboscis Monkey",
Location = "Borneo",
Details = "The proboscis monkey or long-nosed monkey, known as the bekantan in Malay, is a reddish-brown arboreal Old World monkey that is endemic to the south-east Asian island of Borneo.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/Proboscis_Monkey_in_Borneo.jpg/250px-Proboscis_Monkey_in_Borneo.jpg"
});
source.Add(new Monkey
{
Name = "Red-shanked Douc",
Location = "Vietnam, Laos",
Details = "The red-shanked douc is a species of Old World monkey, among the most colourful of all primates. This monkey is sometimes called the \"costumed ape\" for its extravagant appearance. From its knees to its ankles it sports maroon-red \"stockings\", and it appears to wear white forearm length gloves. Its attire is finished with black hands and feet. The golden face is framed by a white ruff, which is considerably fluffier in males. The eyelids are a soft powder blue. The tail is white with a triangle of white hair at the base. Males of all ages have a white spot on both sides of the corners of the rump patch, and red and white genitals.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Portrait_of_a_Douc.jpg/159px-Portrait_of_a_Douc.jpg"
});
source.Add(new Monkey
{
Name = "Gray-shanked Douc",
Location = "Vietnam",
Details = "The gray-shanked douc langur is a douc species native to the Vietnamese provinces of Quảng Nam, Quảng Ngãi, Bình Định, Kon Tum, and Gia Lai. The total population is estimated at 550 to 700 individuals. In 2016, Dr Benjamin Rawson, Country Director of Fauna & Flora International - Vietnam Programme, announced a discovery of an additional population of more than 500 individuals found in Central Vietnam, bringing the total population up to approximately 1000 individuals.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Cuc.Phuong.Primate.Rehab.center.jpg/320px-Cuc.Phuong.Primate.Rehab.center.jpg"
});
source.Add(new Monkey
{
Name = "Golden Snub-nosed Monkey",
Location = "China",
Details = "The golden snub-nosed monkey is an Old World monkey in the Colobinae subfamily. It is endemic to a small area in temperate, mountainous forests of central and Southwest China. They inhabit these mountainous forests of Southwestern China at elevations of 1,500-3,400 m above sea level. The Chinese name is Sichuan golden hair monkey. It is also widely referred to as the Sichuan snub-nosed monkey. Of the three species of snub-nosed monkeys in China, the golden snub-nosed monkey is the most widely distributed throughout China.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Golden_Snub-nosed_Monkeys%2C_Qinling_Mountains_-_China.jpg/165px-Golden_Snub-nosed_Monkeys%2C_Qinling_Mountains_-_China.jpg"
});
source.Add(new Monkey
{
Name = "Black Snub-nosed Monkey",
Location = "China",
Details = "The black snub-nosed monkey, also known as the Yunnan snub-nosed monkey, is an endangered species of primate in the family Cercopithecidae. It is endemic to China, where it is known to the locals as the Yunnan golden hair monkey and the black golden hair monkey. It is threatened by habitat loss. It was named after Bishop Félix Biet.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/RhinopitecusBieti.jpg/320px-RhinopitecusBieti.jpg"
});
source.Add(new Monkey
{
Name = "Tonkin Snub-nosed Monkey",
Location = "Vietnam",
Details = "The Tonkin snub-nosed monkey or Dollman's snub-nosed monkey is a slender-bodied arboreal Old World monkey, endemic to northern Vietnam. It is a black and white monkey with a pink nose and lips and blue patches round the eyes. It is found at altitudes of 200 to 1,200 m (700 to 3,900 ft) on fragmentary patches of forest on craggy limestone areas. First described in 1912, the monkey was rediscovered in 1990 but is exceedingly rare. In 2008, fewer than 250 individuals were thought to exist, and the species was the subject of intense conservation effort. The main threats faced by these monkeys is habitat loss and hunting, and the International Union for Conservation of Nature has rated the species as \"critically endangered\".",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Tonkin_snub-nosed_monkeys_%28Rhinopithecus_avunculus%29.jpg/320px-Tonkin_snub-nosed_monkeys_%28Rhinopithecus_avunculus%29.jpg"
});
source.Add(new Monkey
{
Name = "Thomas's Langur",
Location = "Indonesia",
Details = "Thomas's langur is a species of primate in the family Cercopithecidae. It is endemic to North Sumatra, Indonesia. Its natural habitat is subtropical or tropical dry forests. It is threatened by habitat loss. Its native names are reungkah in Acehnese and kedih in Alas.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Thomas%27s_langur_Presbytis_thomasi.jpg/142px-Thomas%27s_langur_Presbytis_thomasi.jpg"
});
source.Add(new Monkey
{
Name = "Purple-faced Langur",
Location = "Sri Lanka",
Details = "The purple-faced langur, also known as the purple-faced leaf monkey, is a species of Old World monkey that is endemic to Sri Lanka. The animal is a long-tailed arboreal species, identified by a mostly brown appearance, dark face (with paler lower face) and a very shy nature. The species was once highly prevalent, found in suburban Colombo and the \"wet zone\" villages (areas with high temperatures and high humidity throughout the year, whilst rain deluges occur during the monsoon seasons), but rapid urbanization has led to a significant decrease in the population level of the monkeys.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/Semnopithèque_blanchâtre_mâle.JPG/192px-Semnopithèque_blanchâtre_mâle.JPG"
});
source.Add(new Monkey
{
Name = "Gelada",
Location = "Ethiopia",
Details = "The gelada, sometimes called the bleeding-heart monkey or the gelada baboon, is a species of Old World monkey found only in the Ethiopian Highlands, with large populations in the Semien Mountains. Theropithecus is derived from the Greek root words for \"beast-ape.\" Like its close relatives the baboons, it is largely terrestrial, spending much of its time foraging in grasslands.",
ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/13/Gelada-Pavian.jpg/320px-Gelada-Pavian.jpg"
});
Monkeys = new ObservableCollection<Monkey>(source);
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
After MUCH research I found a good solution to this problem.
You should use a BindableLayout instead of any kind of CollectionView or ListView.
BindableLayouts adjust their size according to their contents automatically.
<StackLayout BindableLayout.ItemsSource="{Binding Items}"
Orientation="Vertical">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="{x:Type model:ListItem}">
<VerticalStackLayout>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource
AncestorType={x:Type views:MyViewModel}},
Path=TapOnSelectedItemCommand}"
NumberOfTapsRequired="1"
CommandParameter="{Binding .}"/>
</VerticalStackLayout.GestureRecognizers>
<HorizontalStackLayout>
<Label Text="{Binding ListItem.Name}"/>
<Button Text="..."
Command="{Binding DetailsButtonCommand}"
HorizontalOptions="End"
/>
</HorizontalStackLayout>
<VerticalStackLayout IsVisible="{Binding IsExpanded }">
<Label Text="EXPANDED"/>
<Button Text="Test"
Command="{Binding Source={RelativeSource
AncestorType={x:Type views: MyViewModel}},
Path=DetailsButtonClickedCommand}"
CommandParameter="{Binding .}"/>
</VerticalStackLayout>
</VerticalStackLayout>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
Next I set up a TapGestureRecognizer to detect taps on the items, which would collapse/expand them. They call a command in the code behind that would trigger a bool that was set to the IsVisible component mentioned above.
I track which items are expanded/not expanded, and toggle it accordingly.
I was attempting to do the same thing and while Adam has the right idea, the code was incomplete and took quite some time to figure out.
Also, in the other examples above, CollectionView doesn't support collapsing groups, but BindableLayout does.
I created a full demo project and put it up on github here: https://github.com/sej69/MauiGroupedList
This is what it will look like:
This is the basics of what needs to be done.
(using CommunityToolkit)
The base data model, this is the data which will be grouped in each of the headings.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MauiGrouping.Models
{
public class Animal
{
public string name { get; set; }
public string location { get; set; }
public string details { get; set; }
}
}
The grouping/list data model, this is the grouped lists class, the headings are "string species" in this example.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MauiGrouping.Models
{
// iNotifyPropertyChanged is includede even though we are using community toolkit as
// a class can only inherit one parent class
public class AnimalList : List<Animal>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string species { get; private set; }
// variables to control open / close groupings
private bool _isExpanded = true;
private bool _notExpanded = false;
public bool IsExpanded
{
get => _isExpanded;
set => SetField(ref _isExpanded, value);
}
public bool NotExpanded
{
get => _notExpanded;
set => SetField(ref _notExpanded, value);
}
public List<Animal>? animals { get; private set; }
public AnimalList(string? species, List<Animal>? animals) : base(animals)
{
this.species = species;
this.animals = animals;
}
protected void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}
The reason I'm using the OnPropertyChanged here is because a class in C# can only inherit from a single class. And in order for this to work, we need to inherit from the parent datamodel. This is the only place where the CommunityToolkit is not used.
The View:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiGrouping.Pages.AnimalsPage"
xmlns:viewmodel="clr-namespace:MauiGrouping.ViewModels"
xmlns:dm="clr-namespace:MauiGrouping.Models"
Title="Animals">
<ScrollView VerticalScrollBarVisibility="Always" VerticalOptions="FillAndExpand">
<VerticalStackLayout >
<StackLayout BindableLayout.ItemsSource="{Binding AllAnimals}" Orientation="Vertical">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="dm:AnimalList">
<!-- Header (group info) here -->
<StackLayout>
<Frame BackgroundColor="BlueViolet" MaximumWidthRequest="200" HorizontalOptions="Start">
<HorizontalStackLayout>
<!-- the following will arrow down/right if the grouping is expanded or not -->
<ImageButton Source="down_arrow.png" HeightRequest="25" WidthRequest="25"
Command="{Binding Source={RelativeSource
AncestorType={x:Type viewmodel:AnimalsViewModel}},
Path=ExpandCommand}" CommandParameter="{Binding .}" IsVisible="{Binding IsExpanded}"/>
<ImageButton Source="right_arrow.png" HeightRequest="15" WidthRequest="25"
Command="{Binding Source={RelativeSource
AncestorType={x:Type viewmodel:AnimalsViewModel}},
Path=ExpandCommand}" CommandParameter="{Binding .}" IsVisible="{Binding NotExpanded}"/>
<Label Text= "{Binding species}" Padding="5,0,20,0"/>
</HorizontalStackLayout>
</Frame>
<!-- list data here -->
<StackLayout BindableLayout.ItemsSource="{Binding .}" IsVisible="{Binding IsExpanded}">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="dm:Animal">
<Frame BackgroundColor="DarkGreen" Margin="20,0,15,0">
<VerticalStackLayout>
<HorizontalStackLayout>
<Label Text="{Binding name}" Padding="0,0,30,0" />
<Label Text="{Binding location}" Padding="0,0,5,0"/>
</HorizontalStackLayout>
<Label Text="{Binding details}" Padding="0,0,10,0" />
</VerticalStackLayout>
</Frame>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
The view model:
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MauiGrouping.Models;
using CommunityToolkit.Mvvm.Input;
namespace MauiGrouping.ViewModels
{
public partial class AnimalsViewModel : ObservableObject
{
[ObservableProperty]
public ObservableCollection<AnimalList> allAnimals;
public AnimalsViewModel() { }
// This is a little convoluted but the data needs to be entered in a constructor
// not sure why, but it works
public void LoadData()
{
// initialize list
AllAnimals = new ObservableCollection<AnimalList>();
// first create a list of all animals to be inserted into the observableCollection
// for example purposes only, you'll be importing data from other means
List<Animal> bears = new List<Animal>();
List<Animal> monkeys = new List<Animal>();
monkeys.Add(new Animal
{
name = "Mandrill",
location = "Southern Cameroon, Gabon, Equatorial Guinea, and Congo",
details = "The mandrill is a primate of the Old World Animal family, closely related to the baboons and even more closely to the drill. It is found in southern Cameroon, Gabon, Equatorial Guinea, and Congo."
});
monkeys.Add(new Animal
{
name = "Proboscis Animal",
location = "Borneo",
details = "The proboscis Animal or long-nosed Animal, known as the bekantan in Malay, is a reddish-brown arboreal Old World Animal that is endemic to the south-east Asian island of Borneo."
});
monkeys.Add(new Animal
{
name = "Red-shanked Douc",
location = "Vietnam, Laos",
details = "The red-shanked douc is a species of Old World Animal, among the most colourful of all primates. This Animal is sometimes called the \"costumed ape\" for its extravagant appearance. From its knees to its ankles it sports maroon-red \"stockings\", and it appears to wear white forearm length gloves. Its attire is finished with black hands and feet. The golden face is framed by a white ruff, which is considerably fluffier in males. The eyelids are a soft powder blue. The tail is white with a triangle of white hair at the base. Males of all ages have a white spot on both sides of the corners of the rump patch, and red and white genitals."
});
bears.Add(new Animal
{
name = "Black",
location = "North America",
details = "The black bear is a shy and native bear in North America"
});
bears.Add(new Animal
{
name = "Polar",
location = "Canada",
details = "The polar bear is one of the largest bears on the planet."
});
// now we insert this information into the observable list here
// again, this is messy, but it's to demo the groupings
AnimalList allMonkeys = new AnimalList("Monkeys", monkeys);
AnimalList allBears = new AnimalList("Bears", bears);
// note the capital letter starting the variable here
AllAnimals.Add(allMonkeys);
AllAnimals.Add(allBears);
}
[RelayCommand]
public void Expand(AnimalList arg)
{
arg.IsExpanded = !arg.IsExpanded; // flip / flop buttons
arg.NotExpanded = !arg.NotExpanded;
}
}
}
Again, for a full working demonstration of how this works, see the github repository.
Related
Getting acquainted with UWP. I'm developing an App for simulating electric circuits. There is a classic visual control called Frame, later called GroupBox in WPF.
It seems this control is absent in UWP.
There is a control called HeaderedContentControl in UWP.Toolkit library, but doesn't look the same. And seems the background and border properties don't work..
currently my code is:
<controls:HeaderedContentControl Margin="5"
BorderBrush="Black" BorderThickness="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<controls:HeaderedContentControl.Header>
<Border BorderBrush="Black" BorderThickness="1">
<Border.RenderTransform>
<TranslateTransform Y="-10"/>
</Border.RenderTransform>
<TextBlock Text="Resistor Value"/>
</Border>
</controls:HeaderedContentControl.Header>
<local:ComponentValueBox Unit="Ohm" HorizontalAlignment="Left"
Value="{x:Bind resistorValue, Mode=TwoWay}"
ValueChanged="changeR"/>
</controls:HeaderedContentControl>
And what I see (in the flyout) is:
Not quite like the GroupBox control..
What I would like to see is something like following:
What Should I do?
UWP is different from WPF. UWP is based on windows runtime, WPF is based on .NET Framework. They all use XAML to layout UI elments, but they have different XAML rendering engine. You could not think that MS dropped the old classic control. They're totally on the different platform. We call 'UWP' as Unversal Windows Platform. For now, you're not able to find such a 'GroupBox', but it's a new platform, you might be able to see such a control in the future. Anything is possible.
For your requirement, like #Muzib said, you entirely could make a custom control to meet your requirement. I used UserControl TextBlock Border ContentControl to make such a 'GroupBox' for your reference.
Please see my following code sample:
<UserControl
x:Class="AppGroupBox.GroupBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AppGroupBox"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<TextBlock x:Name="HeaderTitle" Text="Header" Margin="7 0 0 0" LayoutUpdated="HeaderTitle_LayoutUpdated"></TextBlock>
<Border BorderBrush="Black" x:Name="border" BorderThickness="0 2 0 0" Margin="100 10 3 3" CornerRadius="0 5 0 0"></Border>
<Border BorderBrush="Black" BorderThickness="2 0 2 2" Margin="3 10 3 3" CornerRadius="5">
<ContentControl x:Name="Content" Margin="10 10 10 10">
</ContentControl>
</Border>
</Grid>
public sealed partial class GroupBox : UserControl
{
public GroupBox()
{
this.InitializeComponent();
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
// Using a DependencyProperty as the backing store for Header. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(string), typeof(GroupBox), new PropertyMetadata("Your Header", HeaderPropertyChangedCallback));
public static void HeaderPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != e.OldValue)
{
(d as GroupBox).HeaderTitle.Text = e.NewValue?.ToString();
//(d as GroupBox).border.Margin = new Thickness((d as GroupBox).HeaderTitle.ActualWidth, 10, 3, 3);
}
}
public object CustomContent
{
get { return (object)GetValue(CustomContentProperty); }
set { SetValue(CustomContentProperty, value); }
}
// Using a DependencyProperty as the backing store for Content. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CustomContentProperty =
DependencyProperty.Register("CustomContent", typeof(object), typeof(GroupBox), new PropertyMetadata(null,PropertyChangedCallback));
public static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != e.OldValue)
{
(d as GroupBox).Content.Content = e.NewValue;
}
}
private void HeaderTitle_LayoutUpdated(object sender, object e)
{
border.Margin = new Thickness(HeaderTitle.ActualWidth+10,10,3,3);
}
}
<local:GroupBox Header="My GroupBox" Height="300" Width="500">
<local:GroupBox.CustomContent>
<StackPanel>
<RadioButton Content="r1"></RadioButton>
<TextBox></TextBox>
</StackPanel>
</local:GroupBox.CustomContent>
</local:GroupBox>
I don't think there's such controls in UWP. Most probably you have to make your own CustomControl to achieve something that looks exactly lik that in UWP.
But hey, you can achieve something like that with a 'customized' ListView. Look at this:
<ListView Header="I am a header" BorderThickness="1" BorderBrush="Red" Width="250" Height="200" SelectionMode="None">
<ListView.HeaderTemplate>
<DataTemplate>
<ListViewHeaderItem Content="{Binding}"/>
</DataTemplate>
</ListView.HeaderTemplate>
<RadioButton>Any Value</RadioButton>
<RadioButton>1% standard?</RadioButton>
<RadioButton>5% standard</RadioButton>
</ListView>
It produces this output:
Of course You can make these items more dense if you want so.
bit stuck on this.
Have a list view and I want to change the theme to match the rest of my app.
Been following a few examples of how to change the selected item back color which I have working really well using custom renders, mainly this example
https://blog.wislon.io/posts/2017/04/11/xamforms-listview-selected-colour
However no example I've been able to find addresses the fore color of the selected items.
Is that something I would do with custom renders as with the background or am I backing up the wrong tree?
My list view definition is as follows
<ListView.ItemTemplate>
<DataTemplate>
<customControls:ExtendedViewCell SelectedBackgroundColor="#5DB8B3">
<ViewCell.View>
<StackLayout VerticalOptions="StartAndExpand">
<Label Text="{Binding AttributeName}"
FontSize="Small"
FontAttributes="Bold"/>
<Label Text="{Binding Description}"
FontSize="Small"/>
<Label Text="{Binding CreditorName}"
FontSize="Small"/>
</StackLayout>
</ViewCell.View>
</customControls:ExtendedViewCell>
</DataTemplate>
</ListView.ItemTemplate>
Appreciate any feedback thank
You can do this (Without a custom renderer) by adding another property to the object is bound to, and binding TextColor on the label to this new property.
Assuming your bound object looks something like this
public class BoundObject
{
public string AttributeName { get; set; }
public string Description { get; set; }
public string CreditorName { get; set; }
public int id { get; set; }
public Color TextColor { get; set; }
}
XAML
Note the ListView control added, with a name property and an ItemSelected event.
<ListView x:Name="myList" ItemSelected="myListSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout VerticalOptions="StartAndExpand">
<Label Text="{Binding AttributeName}"
FontSize="Small"
FontAttributes="Bold"
TextColor="{Binding TextColor}"
/>
<Label Text="{Binding Description}"
FontSize="Small"
TextColor="{Binding TextColor}"
/>
<Label Text="{Binding CreditorName}"
FontSize="Small"
TextColor="{Binding TextColor}"
/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Code Behind
Most of the magic happens in the code behind. Note that I'm just adding a few items to the list on start here - just for debug purposes. It's important to note that the start color is also given at the time the list needs to be created.
I've also added an ID field to the BoundObject, so we can more easily identify which object we have selected.
List<BoundObject> listItems = new List<BoundObject>();
public YourPage()
{
InitializeComponent();
for (int i = 0; i < 10; i++)
{
listItems.Add(new BoundObject() { id=i, AttributeName = "Attribute " + i, Description = i + " description", CreditorName = "Creditor: " + i, TextColor = Color.Blue });
}
myList.ItemsSource = listItems;
}
private void myListSelected(object sender, SelectedItemChangedEventArgs e)
{
if (((ListView)sender).SelectedItem == null)
return;
//Get the item we have tapped on in the list. Because our ItemsSource is bound to a list of BoundObject, this is possible.
var selection = (BoundObject)e.SelectedItem;
//Loop through our List<BoundObject> - if the item is our selected item (checking on ID) - change the color. Else - set it back to blue
foreach(var item in listItems)
{
if (item.id == selection.id)
item.TextColor = Color.Red;
else
item.TextColor = Color.Blue;
}
//ItemsSource must be set to null before it is re-assigned, otherwise it will not re-generate with the updated values.
myList.ItemsSource = null;
myList.ItemsSource = listItems;
}
The key points to the code-behind are...
New property TextColor on your bound object, of type Color
Store your BoundObject in a List<BoundObject>
When populating your list for the first time, set the TextColor property in your BoundObject
In the ItemSelected event for your list, get the current selection, and update the List<BoundObject> setting the colours as your conditions need
Set the list ItemSource to null, and re-assign it to the (now updated) List<BoundObject>
Can achieve through,
a custom renderer , however with this approach the color is not applied when the cell includes a ContextAction.
Using Custom Renderer,
From bugzilla
Using Cross Platform Way (binding), this approach applying the color to all cells(layout) that including a ContextAction
Obviously in Xamarin Forms,
Possible ways to achevie
Stack Overflow discussion
I'm working on a view (called 'Familify') which shows users a list of assets, and allows them to delete an asset from the list. The assets are stored in an ObservableCollection in the ViewModel, so the command to delete simply takes the asset object and removes it from collection. I'm having issues getting the 'delete' functionality working. Here is the XAML and codebehind:
Familify.xaml
<ListView
ItemsSource="{Binding Assets}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80px" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150px" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="60px" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="{Binding number}" FontFamily="Consolas"/>
<TextBlock
Grid.Column="1"
Text="{Binding type}"/>
<TextBlock
Grid.Column="2"
Text="add binding here"/>
<TextBlock
Grid.Column="3"
Text="add binding here"/>
<Button
Command="{x:Bind ViewModel.RemoveAssetCommand}"
CommandParameter="{Binding}"
Content=""
FontFamily="Segoe MDL2 Assets"
Grid.Column="4">
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Familify.xaml.cs
namespace asset_manager.Views
{
public sealed partial class Familify : UserControl
{
FamilifyViewModel ViewModel { get; set; }
public Familify()
{
this.InitializeComponent();
DataContextChanged += (s, e) =>
{
ViewModel = DataContext as FamilifyViewModel;
};
}
}
}
The idea is that clicking the button removes the asset from the list. (Just to note, the normal binding showing number, type, etc. is working correctly.) My thinking so far:
Try to use binding to access the RemoveAssetCommand stored in the View Model for the page. However, I couldn't get ancestral binding to work (i.e. trying to find the data context of an element higher up in the XAML hierarchy didn't work because findAncestor isn't a thing in UWP.)
x:Bind looked like a good solution, because it uses an explicit path to the property. So, if I declared ViewModel in my code behind, I could use x:Bind ViewModel.property. All well and good. I did just that, and intellisense allowed me to access the ViewModel.RemoveAssetCommand when typing it out.
However, this did not work, because I get the error no DataType defined for DataTemplate. This makes sense, so I tried two things.
x:DataType="Models:Asset" (put in the DataTemplate tag above) is the model being shown in the data template, so I tried that first. Of course, the command is not declared in the model, it's declared in the View Model, so that didn't work.
I instead tried x:DataType="ViewModels:FamilifyViewModel", thinking I could just use x:Bind with that. However, I then got an error that it couldn't cast an object of type Asset to FamilifyViewModel. This makes sense, because the object getting passed to this data template is of the type Asset.
This is a pain, because the whole reason I thought x:Bind would work is that I could just access the property directly from the ViewModel in the codebehind.
Explicitly stated, 1) is it possible to use x:Bind within a data template to access a base level property (in this case, a Prism command) on the ViewModel? and 2) is there a better way to go about implementing this functionality?
Is it possible to use x:Bind within a data template to access a base level property (in this case, a Prism command) on the ViewModel?
Yes, if you want to access a base level, you can reassign DataContext of button like following:
<Button DataContext="{Binding ElementName=Familily, Path=DataContext}"/>
The Family is the name of UserControl.
is there a better way to go about implementing this functionality?
When you put commad in the ViewModel and bind the button as above. The the bind item of button will become Family DataContext. So you could not invoke delete action directly in the ViewModel.
The best practice to implement this functionality is that put the RemoveAssetCommand in the Asset class. And use the ItemsSource of ListView as Button CommandParameter.
<Button
Command="{Binding RemoveAssetCommand}"
CommandParameter="{Binding ElementName=MyListView, Path=ItemsSource}"
Content=""
FontFamily="Segoe MDL2 Assets"
Grid.Column="4">
</Button>
Asset.cs
public class Asset
{
public string number { get; set; }
public string type { get; set; }
public ICommand RemoveAssetCommand
{
get
{
return new CommandHandler<ObservableCollection<Asset>>((item) => this.RemoveAction(item));
}
}
private void RemoveAction(ObservableCollection<Asset> items)
{
items.Remove(this);
}
}
ViewModel.cs
public class FamilifyViewModel
{
public ObservableCollection<Asset> Assets = new ObservableCollection<Asset>();
public FamilifyViewModel()
{
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
}
}
Hard to put into words for title. I have a normal WPF combo box and the data (list of names) is getting pulled from SQL and I want to change the text colour and
Foreground ="Black"
only seems to be working when I actually select the user. Any suggestions how else I can change this?
EDIT: I haven't tried any other things as of yet as I know that way to actually change the text colour.
EDIT2:
<ComboBox x:Name="cmbDepartment" HorizontalAlignment="Left" Height="25" Margin="92,580,0,0" VerticalAlignment="Top" Width="400" Foreground="#FFA2A2A2" FontSize="13"/>
This is my XAML code for the combo box. I have figured out that my theme is making it blue but when I change the font colour on my theme everything then turns that colour in my application. Is there a piece of code that I can write in my XAML which will set the colour of everything in the combo box grey, without changing the colours in my application.
I assume that initially the ComboBox shows no selected value and once you click on it it shows the list of names with the proper color (being whatever color you assigned through the Foreground property).
If so, may it be the case that you haven't selected an item? Once you have set the items, you must select an item (e.g. SelectedIndex, SelectedValue) if you don't want the ComboBox selection to appear empty.
Excuse me if this is not the case, but the question was pretty vague..
Here is a example using MVVM
XAML
<Window x:Class="SelfBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ComboBox ItemsSource="{Binding MyItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Foreground="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
codebehind
using System.Collections.Generic;
using System.Windows;
namespace SelfBinding
{
/// <summary>
/// Interaktionslogik für MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}
public class MyViewModel
{
public List<MyItem> MyItems { get; set; }
public MyViewModel()
{
MyItems = new List<MyItem>();
MyItems.Add(new MyItem { Name = "Black" });
MyItems.Add(new MyItem { Name = "Red" });
MyItems.Add(new MyItem { Name = "Orange" });
MyItems.Add(new MyItem { Name = "Green" });
}
}
public class MyItem
{
public string Name { get; set; }
}
}
to test it on your on create a new WPFproject an copy & past the code
Maybe you can override the theme colors in the combobox resources. This is an exaple for doing so.
I just don't know what exactly is the key that you need to override. I guess you can google that.
good luck.
I'm working on a weather application for windows phone. One of the features that I want to take advantage of is live tiles. I have a background agent that runs when the user pins a city to the start page.
After it's been pinned, it makes a calls out to the internet to get some weather data. All of this works just fine.
Now for the problem.
Depending on the weather data that's returned, I want to update the tiles that are pinned to the start screen.
I have a number of different .xaml files (rain, snow, sun, etc) that represent each tile.
My first thought was that I would:
expose 2 properties on each tile (CityState and Temp)
set those 2 properties after the tile is created.
save the tile off into IsolatedStorage as an image that I can then use to update the tile on the start screen.
Here is the code that I have to do that:
var ctl = new Snow();
//just some dummy data to test
ctl.CityState = "Test, NY";
ctl.Temp = 25;
ctl.Measure(new Size(173, 173));
ctl.Arrange(new Rect(0, 0, 173, 173));
var bmp = new WriteableBitmap(173, 173);
bmp.Render(ctl, null);
bmp.Invalidate();
var iss =IsolatedStorageFile.GetUserStoreForApplication();
var filename = "/Shared/ShellContent/tileTest.jpg";
using (var stm = iss.CreateFile(filename))
{
bmp.SaveJpeg(stm, 173, 173, 0, 80);
}
tile.BackgroundImage = new Uri("isostore:" + filename, UriKind.Absolute);
var tileToUpdate = ShellTile.ActiveTiles.FirstOrDefault(r => r.NavigationUri == uri);
tileToUpdate.Update(tile);
So, when this runs, it creates a new tile from the XAML file and updates the start screen but the Temp and CityState properties
are not reflected on the new Tile. In the xaml I have 2 textblocks that are bound to the properties in the codebehind. I've also
implemented INotifyPropertyChanged.
Here is the XAML
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Name="Window"
x:Class="ezweather.services.tiles.Snow"
d:DesignWidth="480" d:DesignHeight="800" Width="173" Height="173" >
<Canvas x:Name="Layer_1" Width="173" Height="173" Canvas.Left="0" Canvas.Top="0" >
<Rectangle x:Name="Rectangle" Width="173" Height="173" Canvas.Left="0" Canvas.Top="-1.52588e-005" Stretch="Fill" Fill="#FF3F6A8D"/>
<TextBlock x:Name="cityState" TextAlignment="Left" FontFamily="Segoe UI Semibold" FontWeight="Bold" FontSize="15" Width="Auto" Height="Auto" Canvas.Left="0" Canvas.Top="0">
<TextBlock.RenderTransform>
<TransformGroup>
<MatrixTransform Matrix="1.33333,0,0,1.33333,11,139.5"/>
</TransformGroup>
</TextBlock.RenderTransform>
<Run Text="{Binding ElementName=Window, Path=CityState}" Foreground="#FFFFFFFF"/>
</TextBlock>
<TextBlock x:Name="temp" TextAlignment="Right" FontFamily="Segoe UI Light" FontSize="44" Width="Auto" Height="Auto" Canvas.Left="0" Canvas.Top="0">
<TextBlock.RenderTransform>
<TransformGroup>
<MatrixTransform Matrix="1.33333,0,0,1.33333,87.57,42.9333"/>
</TransformGroup>
</TextBlock.RenderTransform>
<Run Text="{Binding ElementName=Window, Path=Temp}" Foreground="#FFFFFFFF"/>
</TextBlock>
</Canvas>
</UserControl>
and here is the codebehind
public partial class Snow : UserControl, INotifyPropertyChanged
{
public Snow()
{
// Required to initialize variables
InitializeComponent();
}
private string _cityState;
private int _temp;
public string CityState
{
get { return _cityState; }
set
{
_cityState = value;
RaisePropertyChanged("CityState");
}
}
public int Temp
{
get { return _temp; }
set
{
_temp = value;
RaisePropertyChanged("Temp");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string property)
{
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
When this code runs, it instantiates the correct xaml file and saves it to disk.
It then updates the tile on the start screen but the CityState and Temp data does not show up.
I don't know why the CityState and Temp data isn't being written out with the image.
What am I missing?
The primary issue I see here, is you're attempting to render the image, before the control is actually loaded.
Try handle the rendering in the Control.Loaded event.