UWP User Controls expose internal StackPanel - controls

I'm relatively new to UWP being a developer used to using Windows forms.
In windows forms what I'm attempting to do I would do quite easily through form inheritance.
UWP seems to have no form of layout inheritance, it appears that you have to design a UserControl to achieve this and then place it on the page and fill the page with the control.
I have a control which contains a 2 column grid. In the first column I have placed a StackPanel and at the moment the second column is empty.
<UserControl x:Name="userControl"
x:Class="App1.BaseLayoutControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="11*"/>
<ColumnDefinition Width="29*"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="0" Background="#FF0B1E70"/>
</Grid>
</UserControl>
And the code:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using App1.Annotations;
// The User Control item template is documented at http://go.microsoft.com/fwlink/?LinkId=234236
namespace App1
{
public sealed partial class BaseLayoutControl : INotifyPropertyChanged
{
public BaseLayoutControl()
{
InitializeComponent();
}
public StackPanel LeftStackPanel
{
get { return ( StackPanel ) GetValue( LeftStackPanelProperty ); }
set
{
SetValue( LeftStackPanelProperty, value );
OnPropertyChanged( nameof( LeftStackPanel ) );
}
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LeftStackPanelProperty =
DependencyProperty.Register( "LeftStackPanel", typeof( StackPanel ), typeof( BaseLayoutControl ), null );
public event PropertyChangedEventHandler PropertyChanged;
[ NotifyPropertyChangedInvocator ]
private void OnPropertyChanged( [ CallerMemberName ] string propertyName = null )
{
PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}
}
}
Now what I want to do is expose the StackPanel as a property "LeftStackPanel" and also at design time where I have placed the control on the page I would like to be able to drag and drop Buttons, CheckBoxes, Text etc into the StackPanel inside the control. Also I would like to expose the second unused grid column so that items can also be place in it.
All the examples I can find seem to only deal with simple object types like ints and strings.
Any help appreciated.
Terry

This is a longtime challenge when migrating from WinForms to XAML.
The short answer is that you should be looking at creating a CustomControl and not a UserControl.
A UserControl is a self-contained UI element that you provide data and it displays it.
When you need to customize not only the information displayed but also the way you display it, you need to use a CustomControl. That will enable the scenarios that you are trying to achieve in your case.
Here is a very good visual tutorial on how to author your first CustomControl. Also, for mode details on the differences between User and Custom control check this post. In fact there are many articles and tutorials out there on this topic. Most of them are from the WPF days, but everything still applies in UWP.

Related

Any alternatives to the switching different groups of Page elements visibility in .net Maui?

I need to build a page which has some static "always there" elements, and some alternate groups of elements that show up and hide depending on user actions.
One approach could be combine each group of elements into some container like StackLayout and control their visibility, like this:
<StackLayout IsVisible="{Binding IsLoaded}">...</StackLayout>
<StackLayout IsVisible="{Binding IsLoaded}, Converter={helpers:InverseBoolConverter}">...</StackLayout>
However if there are more than 2 such groups of such elements, more overhead is added, and I feel like a better way of implementing that must exist.
What I've found is DataTemplateSelector, however: it seems to work for "list of items" type of controls. I wonder if something similar exists for a ContentPage or non-list controls, so I can define 2 or more alternative templates (or controls) with bindings to the same ViewModel inside, and based on the page ViewModel data switch their visibility: ViewModel.IsLoaded=true display one template/control, otherwise display the other.
Note: different control styling won't be enough for my scenario, it's different set of controls.
StackLayout can specify a DataTemplateSelector in its BindableLayout.ItemTemplateSelector:
<StackLayout BindableLayout.ItemsSource="{Binding Source}"
BindableLayout.ItemTemplateSelector="{StaticResource TemplateSelector}"/>
BindableLayout.ItemsSource might be a collection containing one item:
Source = new ObservableCollection<Model>
{
model
};
The DataTemplateSelector might be triggered as follows:
Source.Clear();
Source.Add(model);
Sample DataTemplateSelector:
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate DataTemplate1 { get; set; }
public DataTemplate DataTemplate2 { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
Model model = item as Model;
// Determine template; DataTemplate1 or DataTemplate2
return template;
}
}
Sample usage in XAML:
<DataTemplate x:Key="DataTemplate1">
...
<DataTemplate x:Key="DataTemplate2">
...
<namespace:TemplateSelector x:Key="TemplateSelector"
DataTemplate1="{StaticResource DataTemplate1}"
DataTemplate2="{StaticResource DataTemplate2}" />

Xamarin - Binding to a Property of a child control inside a custom control

I am working on a Xamarin App. I am making a custom control with a Label control, a DatePicker control and an Entry control. I had to create quite a few BindableProperties for the Date Control in the Custom Control such as MaximumDate, MinimumDate Property among many other properties. As far as I understand, the reason I have to create these BindableProperty members in my Custom control are because I have no access to the properties of the child controls when the custom control is used on a view. Is there a way to access the properties of the child control that is embedded in a custom control? I could save a lot of lines of code defining the BindableProperties and their CLR properties and other things.
Here is my Custom control XAML (I have removed all the elements except for a Label element in the posted code to make the code more readable and for brevity.
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DPSReminders.Controls.CustomLabel"
xmlns:controls="clr-namespace:DPSReminders.Controls"
xmlns:sfLayout="clr-namespace:Syncfusion.XForms.TextInputLayout;assembly=Syncfusion.Core.XForms"
xmlns:sfPicker="clr-namespace:Syncfusion.XForms.Pickers;assembly=Syncfusion.SfPicker.XForms"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
xmlns:fai="clr-namespace:FontAwesome">
<StackLayout.Resources>
<ResourceDictionary>
</ResourceDictionary>
</StackLayout.Resources>
<StackLayout Orientation="Horizontal" Margin="10">
<Label x:Name="myLabel"
Text=""
FontFamily="FASolid"
VerticalOptions="Center"
HorizontalOptions="Start"
Margin="10">
</Label>
</StackLayout>
The code behind file:
public class CustomLabel : StackLayout
{
public static readonly BindableProperty LabelTextProperty =
BindableProperty.Create(nameof(LabelText), typeof(string), typeof(CustomLabel),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: LabelTextPropertyChanged);
public string LabelText
{
get => GetValue(LabelTextProperty)?.ToString();
set => SetValue(LabelTextProperty, value);
}
private static void LabelTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = bindable as CustomLabel;
control.myLabel.Text = newValue?.ToString();
}
public CustomLabel()
{
InitializeComponent();
}
}
Here is the page where I am trying to use the custom control.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DPSReminders.Views.DateTimeTest"
xmlns:fai="clr-namespace:FontAwesome"
xmlns:vm="clr-namespace:DPSReminders.ViewModels"
xmlns:controls="clr-namespace:DPSReminders.Controls"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
>
<controls:CustomLabel LabelText = "{Binding MyLabelText}"/>
</ContentPage>
I am wondering if I could do a line like this in my label, that would make my life much easier.
<controls:CustomLabel:myLabel.Text = "{Binding MyLabelText}"/>
Then, I can remove all the code creating the BindableProperties and the supporting CLR properties etc. when a built-in bindable property for the same purpose is already available in the child control. Is this something we can do?
Try using template instead.
Xamarin.Forms control templates enable you to define the visual structure of ContentView derived custom controls, and ContentPage derived pages. Control templates separate the user interface (UI) for a custom control, or page, from the logic that implements the control or page. Additional content can also be inserted into the templated custom control, or templated page, at a pre-defined location.
Doc link:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/templates/control-template

Xamarin Forms ListView not Updating from ObservableCollection

The ListView of my XAML file is being filled with a ViewModel that has an ObservableCollection from a service but the ListView is not showing the information. I already check that the service is returning the correct information.
This is the code of my XML file :
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XAMLUnit1.Views.NetflixRoulettePage" Title="Movie">
<ContentPage.Content>
<StackLayout>
<AbsoluteLayout>
<BoxView Color="Gray" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1"></BoxView>
<SearchBar x:Name="searchBar"
Placeholder="Search By Actor's name"
PlaceholderColor="White"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0.1,0.1,1,1" SearchCommand="{Binding SearchMovieCommand}" ></SearchBar>
</AbsoluteLayout>
<ListView x:Name="ListOfMovies"
ItemsSource="{ Binding MovieList}" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<ImageCell
ImageSource="{Binding poster_path, StringFormat='https://image.tmdb.org/t/p/w500{0}'}">
</ImageCell>
<StackLayout Orientation="Horizontal">
<TextCell Detail="{Binding title}"></TextCell>
<TextCell Detail="{Binding release_date}"></TextCell>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
This is the ViewModel that calls the service and it's uses its ObservableCollection as ItemsSource for the ListView :
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
using XAMLUnit1.Models;
using XAMLUnit1.ServiceImpl;
using XAMLUnit1.Services;
namespace XAMLUnit1.ViewModels
{
public class MovieRouletteViewModel
{
IMovieService service;
public ObservableCollection<Movie> MovieList { get; set; }
public ICommand SearchMovieCommand { get; set; }
public MovieRouletteViewModel()
{
service = new MovieServiceFinder();
SearchMovieCommand = new Command(GetMovies);
}
private void GetMovies()
{ var list= service.GetMovies("");
MovieList = list;
}
}
}
public partial class NetflixRoulettePage : ContentPage
{
MovieRouletteViewModel viewModel;
public NetflixRoulettePage()
{
InitializeComponent();
viewModel = new MovieRouletteViewModel();
BindingContext = viewModel;
}
private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
{
}
}
Do not set the ObservableCollection to a new List it will break the binding. Clear the items from the list and add the new items to it.
public MovieRouletteViewModel()
{
service = new MovieServiceFinder();
SearchMovieCommand = new Command(GetMovies);
MovieList = new ObservableCollection<Movie>();
}
private void GetMovies()
{
var list= service.GetMovies("");
MovieList.Clear();
foreach(Movie movie in list)
{
MovieList.Add(movie);
}
}
There is a few things i want to note on this problem, though mainly as the comments rightly illuminate, replacing the collection with a new collection breaks the bindings. hence why its not updating.
However there are several solutions to consider.
Implement INotifyPropertyChanged on the collection and replace the whole collection like you are doing, This will work fine. However it does resets the scroll and basically recalculates everything from scratch. Some would say this defeats the point of an ObservableCollection as they have their own built in notification plumbing, Yeah it does. Though, if you are replacing the whole Collection with a new collection then you are going to save on oodles of Cyclic calculations when compared to cleaRing and adding each item back individually Which will basically fire for every update
Call Clear and Add on each item individually.. If the collection hasn't changed much, you can even take it a step further by just comparing the 2 collections and updating whats needed. However once again, if the collections are dissimilar, then this approach is still expensive, and on a mobile device you want to minimize screen recalculations where ever possible .
Create a subclassed collection, implement your own replace/update, and Add/Remove range methods, and INotifyPropertyChanged giving you the best of both worlds, this will allow atomic modification of the collection and then you can fire property changed event once.
There are many examples of all these approaches online, its just worth noting clearing and adding items sometimes is not the best approach for mobile devices. It depends how big your collections are, how much is changing and why.
If it is a completely different list, then replacing the collection is fine in my opinion.
If its the same list, then well you may want to compare and modify to save on property changes
Anyway good luck
I disagree with other answers. You can set the Movies as many times as you want, this is not the problem.
The problem is just that your viewmodel doesn't implement INotifyPropertyChanged.
Your UI is just not notified when you set your ObservableCollection.
Your service command is actually replacing the ObservableCollection, you need to change your GetMovies() method to
var list = service.GetMovies("");
MovieList.clear();
foreach(Movie m in list)
{ MovieList.Add(m);}

How to preview an ItemTemplate in the XAML designer

Say I have a simple ItemTemplate for a ListView
<ListView ItemsSource="{x:Bind ListItems}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:TextItem">
<TextBlock Text="{x:Bind Item}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
MainPage.xaml
When I run the app, I get a ListView populated with TextBlocks - one for each item in ListItems (which is a behind defined variable). However, in the XAML designer, I see nothing.
So, is there a way to preview the ItemTemplate/DataTemplate in the XAML designer, with a set number of placeholder TextBlocks with placeholder text replacing the Text="{x:Bind Item}"? Or just preview a single TextBlock?
This is not a duplicate of
Design View of a DataTemplate in a ResourceDictionary in Visual Studio - because I can't use Blend. Everything I've looked up that says use Blend in a certain way merely gives me the message 'This feature is not available for projects targeting "Windows 10 Fall Creators Update (10.0; Build 16299)"'
Can I preview my DataTemplate(s) in my collection-based controls in the designer? - well, perhaps it is, but I don't understand the accepted answer, which is now several years old. An answer containing an example tailored to my question would be really helpful.
Microsoft actually posted a (semi helpful) tutorial on this in April of this year: link
Requirements
Whatever you want to display, you will need this at the top of your XAML code (should be there by default if you created the page using VS's templates):
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
A simple list of strings
For something simple like a list of strings, something like this will work just fine (taken from the above mentioned MSDN page):
<ListView>
<d:ListView.Items>
<system:String>Item One</system:String>
<system:String>Item Two</system:String>
<system:String>Item Three</system:String>
</d:ListView.Items>
</ListView>
Display something more complex
If you want to use a data source from somewhere else in your code, it gets a little more ugly:
First you need to set your data context (usually done at the page level, but you can also add it just on the element where you want to use it):
d:DataContext="{d:DesignInstance
yournamespace:YourDataSourceClass,
IsDesignTimeCreatable=True}"
And on the ListView add:
d:ItemsSource="{Binding data}"
If you are using an ItemTemplate, you will need to add "d:" variants for everything you want to pull from your data source.
Full example code:
XAML:
<Page
x:Class="exampleApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:exampleApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:DesignTimeData,
IsDesignTimeCreatable=True}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Grid>
<ListView ItemsSource="{x:Bind ListItems}" d:ItemsSource="{Binding DummyData}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:TextItem">
<TextBlock Text="{x:Bind Item}" d:Text="{Binding Item}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Grid>
</Page>
C#:
using System.Collections.ObjectModel;
using Windows.UI.Xaml.Controls;
namespace exampleApp {
public sealed partial class MainPage : Page {
public ObservableCollection<TextItem> ListItems;
public MainPage() {
this.InitializeComponent();
//populate you ListItems list
}
}
public class TextItem {
public string Item { get; set; }
}
public class DesignTimeData {
public ObservableCollection<TextItem> DummyData { get; set; } = new ObservableCollection<TextItem> {
new TextItem { Item = "foo" },
new TextItem { Item = "bar" },
new TextItem { Item = "bla" },
new TextItem { Item = "blubb" }
};
}
}
Remarks
It seems you have to use "Binding" rather than "x:Bind" for the design-time bindings. From what I understand, x:Bind works using code generated at compile time, which is not executed by the XAML designer
When you change your Binding declarations or data source code, you will have to recompile to see the changes in the designer. Other design changes will reflect in real time
Binding is more limited than x:Bind in some regards (notably you cannot bind to class methods using Binding, only x:Bind). Keep that in mind when writing your design-time data
Sadly some of the more convenient ways of getting design-time data into your app are WPF and Xamarin only (Blends' data window, data declared in XAML file, and especially auto-generated sample data)
(Everything was tested using Visual Studio 2019 version 16.10.3.)

In Prism (Composite Application Guidelines), how can I get views dynamically loaded into TabControl?

In a Prism v2 application, I define two regions, each a tabitem in a tabcontrol:
<UniformGrid Margin="10">
<TabControl>
<TabItem Header="First" Name="MainRegion" cal:RegionManager.RegionName="MainRegion"/>
<TabItem Header="Second" Name="SecondRegion" cal:RegionManager.RegionName="SecondRegion"/>
</TabControl>
</UniformGrid>
In the bootstrapper two modules are loaded and each injects a view into each of the tabitems:
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog();
catalog.AddModule(typeof(SecondModule.SecondModule));
catalog.AddModule(typeof(HelloWorldModule.HelloWorldModule));
return catalog;
}
Now, of course, I want to perform the decoupling magic that I keep reading about and uncomment one of the modules and see its tab item not appear at all. Instead, on the contrary, there are still two TabItems and one is empty. This tells me that my application is still tightly coupling data and UI as in the bad old WinForm days.
So what do I need to do here to make this dynamic, so that the UI changes dynamically based on what modules are loaded, i.e. so that I could load 10 modules/views in my bootstrapper and there would automatically be 10 TabItems in the TabControl?
INTERMEDIATE ANSWER:
If I just make one region in a TabControl:
<TabControl Name="MainRegion" cal:RegionManager.RegionName="MainRegion"/>
and then load both controls into the MainRegion:
public void Initialize()
{
regionManager.RegisterViewWithRegion("MainRegion", typeof(Views.SecondView));
}
...
public void Initialize()
{
regionManager.RegisterViewWithRegion("MainRegion", typeof(Views.HelloWorldView));
}
then I get a TabControl with two tabs, each with a view in it, which is what I want.
But the TabItem headers are not defined. How do I dynamically define the header (e.g. not in the XAML but dynamically back in the View classes)?
This works too:
public class View : UserControl
{
public string ViewName { get; set; }
}
and then in the shell:
<Window.Resources>
<Style TargetType="{x:Type TabItem}" x:Key="TabItemRegionStyle">
<Setter Property="Header" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.ViewName}" />
</Style>
</Window.Resources>
...
<TabControl cal:RegionManager.RegionName="RightRegion" Width="Auto" Height="Auto" HorizontalAlignment="Stretch" Grid.Column="2"
x:Name="RightRegion" ItemContainerStyle="{StaticResource TabItemRegionStyle}" />
Nice.
You can remove the ViwewName property on the view and change the binding on the TabItem value to be Value="{Binding DataContext.HeaderInfo}" ... where HeaderInfo is a property of your DataContext object - IE the business object which the Tab Item represents. This is a little more elegant.
You are on the right track with your modification.
The way I usually achieve the header is by adding an object to the region instead of a control, and datatemplating it with the control.
This object defines a property (let's say MyHeaderProperty) which I then use to bind to using an ItemContainerStyle on the TabControl.
I do not know if there is a way to achieve that without resorting to that kind of trick (an intermediate object and a DataTemplate).

Resources