Changing part of view at runtime - xaml

I show several movie items in an ObservableCollection using a typical listbox+datatemplate view.
However, I want, in the same page, to be able to quickly change the view to what I define a posterview (i.e. only the posterimages in a wrappanel).
The xaml-page uses a viewmodel as datacontext.
Is there a way to basically replace part of the XAML content with another?
And still keep as little code as possible in the codebehind of the view.
I've seen WPF examples that for example use a DataTrigger bound to a viewmodelproperty which is very clean,
such as this article
... but Windows Phone does not have a DataTriggers, correct?
I'm trying to go for an MVVM-ish approach, so as little code as possible in the view code-behind is required.
So I want to change this:
<ContentControl DataContext="{Binding CinemaShowsOverview }" Template="{StaticResource ListView}" />
To:
<ContentControl DataContext="{Binding CinemaShowsOverview }" Template="{StaticResource PosterView}" />

DataTemplates with a DataTemplateSelector would be the way to go around this problem.
Base Data Template Selector:
public class DataTemplateSelector : ContentControl
{
public virtual DataTemplate SelectTemplate(object item, DependencyObject container)
{
throw new NotImplementedException();
}
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
ContentTemplate = SelectTemplate(newContent, this);
}
}
Specialized Template Selector for your CinemaShowsOverview
public class CinemaShowsTemplateSelector : DataTemplateSelector
{
public DataTemplate ListTemplate
{
get;
set;
}
public DataTemplate PosterTemplate
{
get;
set;
}
public DataTemplate DefaultTemplate
{
get;
set;
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == null)
return DefaultTemplate;
var viewModel = item as CinemaShowsOverview;
if (viewModel != null)
return viewModel.IsPoster ? PowerTemplate : ListTemplate;
else
return DefaultTemplate;
}
}
And then in XAML (replacing your current ContentControl):
<assets:CinemaShowsTemplateSelector PosterTemplate="{StaticResource PosterView}"
ListTemplate="{StaticResource ListView}"
Content="{Binding CinemaShowsOverview}">

Just to be pedantic, the blog you mention describes typed data-templates, not datatriggers (as the author class them). No, this feature is not available in Silverlight for WP7.
You could expose the template you requires as a string within your view model, i.e. a string that is either ListView or PosterView. You then bind your Template property to this view-model property via a value converter that provides the template, which it can access via your applications Resources.

Related

UWP XAML Intellisense DataTemplate.DataType

Why does intellisense filter out interfaces and abstract classes? If I set DataType to an abstract class, it seems to still work fine. Perhaps this is just a bug? Also, related, inside DataTemplate, when I try to {x:Bind} it filters out inherited properties, so if I have Item : Base, and Base has a property Name, and DataType="Item", it filters out property Name and if I use it anyway, it seems to resolve to the class name. Did I miss something in the docs? Should I be making special non-abstract wrapping classes for every type I want to bind to xaml controls?
After my testing, it seems that inherited interface-properties are not recognized by the compiler when using the X:Bind. But it applies to abstract classes.
You could follow the sample to check your steps.
XAML code:
<ListView x:Name="List" ItemsSource="{x:Bind Fruits}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Fruit">
<TextBlock Text="{x:Bind price}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Code behind:
public sealed partial class MainPage : Page
{
public ObservableCollection<Fruit> Fruits{get;set;}
public MainPage()
{
this.InitializeComponent();
Fruits = new ObservableCollection<Fruit>()
{
new Fruit(){name="apple",price=12},
new Fruit(){name="peach",price=15},
new Fruit(){name="pear",price=8},
new Fruit(){name="banana",price=31},
new Fruit(){name="grape",price=5}
};
}
}
public class Fruit: IFruit
{
public string name { get; set;}
}
public abstract class IFruit
{
public int price { get; set;}
}

BindableProperty in ViewCell

I've transferred my ViewCell from the list-xaml to a separate one and now I struggle with binding the properties.
My ListView's item source is an ObservableCollection of "member".
public class Member
{
public string Name { get; set; }
public string Image { get; set; }
public string Description { get; set; }
}
So now I'm trying to figure out how to use the bindable property within my MemberViewCell.
The examples I read are pretty straight forward, I create a BindableProperty for Name as well as a normal property for Name, when Name is modified it will trigger the graphical update.
Can I create a BindableProperty with the basis of Member so that I don't need to write all those bindables/property changed for each property I want exposed?
Something like this:
public static readonly BindableProperty MemberSource = BindableProperty.Create("Member", typeof(Member), typeof(ListViewMemberCell), null, defaultBindingMode: BindingMode.OneWay, propertyChanged: MemberSourcePropertyChanged);
public Member Member
{
get { return GetValue(MemberSource) as Member; }
set
{
SetValue(MemberSource, value);
}
}
private static void MemberSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
ListViewMemberCell cell = (ListViewMemberCell)bindable;
/// Set the different properties to my labels/images based on member.
}
Naturally I tried this first but as expected I get a compile error (within the consumer of this component).
<ListView.ItemTemplate>
<DataTemplate>
<memberCell:ListViewMemberCell Member="{Bindable}"/>
</DataTemplate>
</ListView.ItemTemplate>
So, {Bindable} feels wrong and the error says so as well, but what I did when I had the ViewCell within the DataTemplate was to bind like this {Bindable Name} /// Member.Name...
I hope its just me misunderstanding the examples..
First you'll need to name it MemberSourceProperty. The Property suffix is a requirement for every prop you create.
<ListView ItemsSource="{Binding ListOfMembers}">
<ListView.ItemTemplate>
<DataTemplate>
<!-- This will bind to the object being iterated (Member) -->
<memberCell:ListViewMemberCell MemberSourceProperty="{Bindable .}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And of course you will need to load memberCell in the xaml headers. ListOfMembers should be an ObservableCollection<Member> that you have in the BindingContext of the View.
I'm assuming you will know what to do in the ListViewMemberCell class. To get the name or image, simply do: Member.Name Member.Image ..
Hope it helps

Xamarin forms (Cross-Platform) : Multiple type of cells in ListView

I am new to Xamarin. I have a requirement where I have to implement a ListView or say tableView that have multiple different type-size cells.
And I also have to add Header for a particular section of cells, and some of my custom cells have a horizontal scroll in it.
I have done this thing in iOS native UITableView before, but don't know how this done in Xamarin cross platform, can anyone help me out this?
You are looking for DataTemplateSelector, which is very well documented in the official Xamarin.Forms documentation.
The basics are that you create your own DataTemplateSelector class:
public class MyDataTemplateSelector : DataTemplateSelector
{
}
In that class you override OnSelectTemplate:
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
}
By checking the type of the item argument, you should be able to figure out which template to return.
So lets say you have a ViewModel for Dog and one for Cat and want to show a different DataTemplate for each of those. You would do something like:
public class DogCatTemplateSelector : DataTemplateSelector
{
public DataTemplate DogTemplate { get; set; }
public DataTemplate CatTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if (item is DogViewModel)
return DogTemplate;
return CatTemplate;
}
}
Then you can consume this in your XAML:
<ContentPage.Resources>
<ResourceDictionary>
<DataTemplate x:Key="dogTemplate">
<ViewCell>
... <---- define your look of dog template here
</ViewCell>
</DataTemplate>
<DataTemplate x:Key="catTemplate">
<ViewCell>
... <---- define your look of cat template here
</ViewCell>
</DataTemplate>
<local:DogCatTemplateSelector x:Key="dogCatTemplateSelector"
DogTemplate="{StaticResource dogTemplate}"
CatTemplate="{StaticResource catTemplate}" />
</ResourceDictionary>
</ContentPage.Resources>
Then simply set the ItemTemplate to your dogCatTemplateSelector instance you've defined in the resources on your ListView:
<ListView ItemsSource="{Binding DogsCatsCollection}" ItemTemplate="{StaticResource dogCatTemplateSelector}" />
Your ViewModel would then look something like:
public class Animal : INotifyPropertyChanged
{
}
public class DogViewModel : Animal
{
}
public class CatViewModel : Animal
{
}
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<Animal> DogsCatsCollection { get; }
= new ObservableCollection<Animal>();
}
Then you just populate DogsCatsCollection with instances of dogs and cats.

Xamarin.Forms (XAML): Different layouts depending on a condition

Is there a way to choose what layout initialize depending on one condition? I have a Grid for football stats but if myViewModel.Sport == Sports.Basketball I'd like to load a completely different layout.
I tried something like this with Datatrigger in each View but it seems a mess for me:
<Label Text="{Binding Goals}"
Grid.Row="1" Grid.Column="0">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Sport}"
Value="1">
<Setter Property="Text"
Value="{Binding Points}"/>
</DataTrigger>
</Label.Triggers>
</Label>
I show "goals" but if the Sports enum value is 1 (Sports.Basketball) I change to "points". I want to do this with lots of Labels and even Images so I need a proper way to do it.
Could someone help me? I need to load a different Grid depending on the Sport Property of my ViewModel.
Another thing you could do is place each separate sport into it's own view, add all the views to your page and set their IsVisible property depending on which sport you want to show.
An example would look like this in pseudo-code:
<Page>
<Grid>
<BasketballView IsVisible="{Binding IsBasketball}">
<SoccerView IsVisible="{Binding IsSoccer}">
<FootballView IsVisible="{Binding IsFootball}">
</Grid>
</Page>
Then set the appropriate boolean values from the ViewModel.
To use DataTemplateSelector to solve this, as mentioned by #StephaneDelcroix, you'll want a custom class that has ItemsSource and ItemTemplate properties.
I haven't thought through / tested how DataTemplateSelector would be used with this; anyone is welcome to add that to this answer.
using System.Collections;
using Xamarin.Forms;
namespace YourNamespace
{
// From https://forums.xamarin.com/discussion/19874/listview-inside-stacklayout-a-height-problem/p2, #maxx313.
public class TemplatedStack : StackLayout
{
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IList), typeof(TemplatedStack), propertyChanged: OnItemsSourceChanged);
public IList ItemsSource
{
get { return (IList)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private static void OnItemsSourceChanged(BindableObject pObj, object pOldVal, object pNewVal)
{
var layout = pObj as TemplatedStack;
if (layout != null && layout.ItemTemplate != null)
{
layout.BuildLayout();
layout.ForceLayout();
}
}
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(TemplatedStack), propertyChanged: OnItemTemplateChanged);
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
private static void OnItemTemplateChanged(BindableObject pObj, object pOldVal, object pNewVal)
{
var layout = pObj as TemplatedStack;
if (layout != null && layout.ItemsSource != null)
layout.BuildLayout();
}
private void BuildLayout()
{
Children.Clear();
foreach (var item in ItemsSource)
{
var view = (View)ItemTemplate.CreateContent();
view.BindingContext = item;
Children.Add(view);
}
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
return base.OnMeasure(widthConstraint, heightConstraint);
}
}
}
In your XAML, do
<yourXmlns:TemplatedStack .../>
where yourXmlns must be an xmlns declaration at top of your XAML.
Usage of ItemsSource and ItemTemplate properties is similar to how you would bind an items collection and template to a ListView.
(The reason NOT to use a ListView here, is that ListView may interfere with touch events, and adds extra layout cost.)
Bind to this a collection containing a single item.
E.g. for this question, that item would be the specific sport being viewed.

Updating a GridView after adding an item to a nested list

While I was developing a startscreen for my app using the GridView control, I run into a problem. I have a GridView on my main screen which has a CollectionViewSource set as ItemSource.
For this CollectionViewSource the source is set to an ObservableCollection list. Each GroupViewModel has a ObservableCollection in it. In code the important parts looks like the following:
public class StartPageViewModel : ViewModelBase
{
public ObservableCollection<GroupViewModel> Groups { get; set; }
public CollectionViewSource GroupsCvs { get; set; }
public StartPageViewModel()
{
// fill Groups with some mock data
GroupsCvs.Source = Groups;
GroupsCvs.IsSourceGrouped = true;
}
public void MoveItems(GroupViewModel grp)
{
// add a dummy item
grp.AddRecipe(new ItemViewModel(new Item()) { Id = "123" });
RaisePropertyChanged("GroupsCvs");
RaisePropertyChanged("Groups");
}
}
public class GroupViewModel : ViewModelBase, IEnumerable<ItemViewModel>
{
public ObservableCollection<ItemViewModel> Items { get; set; }
}
View:
public sealed partial class MainPage : LayoutAwarePage
{
private ViewModelLocator locator = new ViewModelLocator();
public MainPage()
{
this.InitializeComponent();
this.DataContext = locator.Main; // returns StartPageViewModel
}
}
XAML part for MainPage, GridView
<GridView ItemsSource="{Binding GroupsCvs.View}" ...
</GridView>
How is it possible to get the UI refreshed when I add an Item to a Group's collection? In my StartPageViewModel I'm adding dummy item to the GroupViewModel and I raise propertychanged, but the Grid remains the same.
I've also tried to fire property changed event in the GroupViewModel class, when the Items collection changes without any luck.
Edit: As I wrote in comments it's possible to refresh with reassigning the source property however this gets the GridView rendered again which is not nice. I'm looking to options which would result in a nicer user experience.
I suppose CollectionViewSource doesn't react to PropertyChanged event. Try reassigning Source to GroupCvs after you modify it. It's not elegant but it should work:
GroupsCvs.Source = Groups;
As a last resort you could create a new instance of ObservableCollection<GroupViewModel> before reassigning it:
Groups = new ObservableCollection<GroupViewModel>(Groups)
GroupsCvs.Source = Groups;
<GridView ItemsSource="{Binding GroupsCvs.View, **BindingMode=TwoWay**}" ...
</GridView>