I need some binding help on my Uno UWP Project.
I have the following objects:
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Picture> Pictures {get;set;}
public int ImageWidth {get;set;}
}
public class Picture : INotifyPropertyChanged
{
public string URL {get;set;}
public int ImageWidth {get;set;}
}
I have the following view:
<Page>
<Grid>
<Grid.RowDefinition>
<RowDefinition Height=*/>
<RowDefinition Height="Auto"/>
</Grid.Rowdefinition>
<GridView ItemsSource="{Binding Pictures}" SelectionMode="Extended" IsMultiSelectCheckBoxEnabled="False"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch" SelectionChanged="ImageSelectionChanged" >
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="Margin" Value="10"/>
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid MaximumRowsOrColumns="5" Orientation="Horizontal"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate>
<Grid Margin="4">
<Image Source="{Binding URL}" Width="{Binding ImageWidth,Mode=TwoWay}" MinWidth="200"/>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<Slider Grid.Row="1" Width="200" Minimum="200" Maximum="1500" StepFrequency="100" TickPlacement="Outside" VerticalAlignment="Bottom" Margin="20,0"
Value="{Binding ImageWidth, Mode=TwoWay}" />
</Grid>
</Page>
The Page DataContext is ViewModel. This works, because the Picture class has an ImageWidth. Instead, I would like to bind to the ViewModel.ImageWidth property.
How can I do this in Uno?
Assuming you want your Slider to change the width of all pictures,
you can share the ImageWidth by changing your entities like this:
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Picture> Pictures {get;set;}
public int ImageWidth {get;set;}
}
public class Picture : INotifyPropertyChanged
{
public ViewModel Parent {get;}
public string URL {get;set;}
}
and then use the following binding in the item template:
<Image Source="{Binding URL}"
Width="{Binding Parent.ImageWidth}"
MinWidth="200"/>
Related
I am working on providing wishlist feature for my app by tapping wishlist icon on each product in list through MVVM. Once tapped, an API call is made to update database(add/remove from wishlist table). Based on result from api call, I updated the specific product's respective property to either 'True' or 'False'. Once property updated, I want to change the icon image source of corresponding product. I am using trigger on wishlist icon to differentiate non-wishlist and wiahlist products while binding the list itself.
My code is below,
MODEL
public class PublisherProducts
{
public long ProductId { get; set; }
public string ProductName { get; set; }
public string ImageURL { get; set; }
public decimal Price { get; set; }
public bool IsWishlistProduct { get; set; }
}
VIEWMODEL
public class OnlineStoreViewModel : BaseViewModel
{
private ObservableCollection<PublisherProducts> publisherProducts;
public Command<long> WishlistTapCommand { get; }
public OnlineStoreViewModel()
{
publisherProducts = new ObservableCollection<PublisherProducts>();
WishlistTapCommand = new Command<long>(OnWishlistSelected);
}
public ObservableCollection<PublisherProducts> PublisherProducts
{
get { return publisherProducts; }
set
{
publisherProducts = value;
OnPropertyChanged();
}
}
public async Task GetProducts(long selectedCategoryId)
{
try
{
...
PublisherProducts = new ObservableCollection<PublisherProducts>(apiresponse.ProductList);
...
}
catch (Exception ex) { ... }
finally { ... }
}
async void OnWishlistSelected(long tappedProductId)
{
if (tappedProductId <= 0)
return;
else
await UpdateWishlist(tappedProductId);
}
public async Task UpdateWishlist(long productId)
{
try
{
var wishlistResponse = // api call
var item = PublisherProducts.Where(p => p.ProductId == productId).FirstOrDefault();
item.IsWishlistProduct = !item.IsWishlistProduct;
PublisherProducts = publisherProducts; *Stuck here to toggle wishlist icon*
await App.Current.MainPage.DisplayAlert("", wishlistResponse.Message, "Ok");
}
catch (Exception ex) { ... }
finally { ... }
}
}
XAML
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" ... >
<ContentPage.Content>
<ScrollView>
<StackLayout Padding="15,0,15,10">
<FlexLayout x:Name="flxLayout" BindableLayout.ItemsSource="{Binding PublisherProducts}" ...>
<BindableLayout.ItemTemplate>
<DataTemplate>
<AbsoluteLayout Margin="6" WidthRequest="150">
<Frame Padding="0" WidthRequest="150" CornerRadius="10" HasShadow="True">
<StackLayout Orientation="Vertical" Padding="10" HorizontalOptions="FillAndExpand">
<Image Source="{Binding ImageURL}" WidthRequest="130" HeightRequest="130" HorizontalOptions="Center"/>
<Label Text="{Binding ProductName}" Style="{StaticResource ProductNameStyle}"></Label>
...
<StackLayout ...>
...
<Frame x:Name="wlistFrame" Padding="0" WidthRequest="30" HeightRequest="30" CornerRadius="10" BorderColor="#02457A">
<StackLayout Orientation="Horizontal" VerticalOptions="Center" HorizontalOptions="Center">
<Image x:Name="wlImage" WidthRequest="13" HeightRequest="12" HorizontalOptions="Center" VerticalOptions="Center" Source="ic_wishlist_open">
<Image.Triggers>
<DataTrigger TargetType="Image" Binding="{Binding IsWishlistProduct}" Value="true">
<Setter Property="Source" Value="ic_wishlist_close" />
</DataTrigger>
</Image.Triggers>
</Image>
</StackLayout>
<Frame.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type local:OnlineStoreViewModel}}, Path=WishlistTapCommand}" CommandParameter="{Binding ProductId}" NumberOfTapsRequired="1" />
</Frame.GestureRecognizers>
</Frame>
</StackLayout>
</StackLayout>
</Frame>
</AbsoluteLayout>
</DataTemplate>
</BindableLayout.ItemTemplate>
</FlexLayout>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
I am stuck at this place to change wishlist icon, when 'IsWishlistProduct' property value is changed in UpdateWishlist().
Guessing through your code, the BaseViewModel contains code similar to the following:
public class BaseViewModel : INotifyPropertyChanged
{
...
public event PropertyChangedEventHandler PropertyChanged;
...
public void OnPropertyChanged(string name)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
...
}
And your viewmodel should be like this:
...
public ObservableCollection<PublisherProducts> PublisherProducts
{
get { return publisherProducts; }
set
{
publisherProducts = value;
OnPropertyChanged(nameof(PublisherProducts));
}
}
...
As Jason mentioned, If there is a change in data in the ViewModel, it is reflected in the UI when it is notified to the View through NotifyPropertyChanged. You already implemented "OnPropertyChanged" function in your BaseViewModel but it seems you don't pass the object name.
I have a BasePage created using ControlTemplates that contains a loading overlay for each child to use - This overlay has a "cancel" button on it, but for some reason I can't get ICommands to execute when I tap the buttons. Clicked events work fine but I'd like to understand what the problem is with Commands.
I researched the issue and found I should be binding using Command="{TemplateBinding MyCommand}" since my content is within a ControlTemplate but still no luck, however I am also binding the Text property and this is working fine, so I'm a bit confused.
Here's a cut down version of what I've hacked together.
Here's my BasePage XAML with the button in question:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage x:Name="this" NavigationPage.HasNavigationBar="False" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:helpers="clr-namespace:ShoppingListNEW.MarkupExtensions" xmlns:views="clr-namespace:ShoppingListNEW.Views" x:Class="ShoppingListNEW.Pages.BasePage">
<ContentPage.ControlTemplate>
<ControlTemplate>
<Grid Padding="0" RowSpacing="0" ColumnSpacing="0">
<StackLayout>
<StackLayout.Padding>
<OnPlatform x:TypeArguments="Thickness" iOS="0,30,0,0"/>
</StackLayout.Padding>
<ScrollView VerticalOptions="FillAndExpand" >
<StackLayout RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToParent, Property=Width}" RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToParent, Property=Height}" Orientation="Vertical">
<ContentPresenter />
</StackLayout>
</ScrollView>
</StackLayout>
<AbsoluteLayout IsVisible="False" Grid.Row="0" Grid.Column="0" x:Name="loading" BackgroundColor="#85000000" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1">
<ActivityIndicator Color="Lime" Scale="2" IsRunning="true" IsEnabled="true" IsVisible="true" AbsoluteLayout.LayoutFlags="PositionProportional" AbsoluteLayout.LayoutBounds="0.5,0.4" />
<Button IsVisible="True" Text="{TemplateBinding MyTextLabel}" AbsoluteLayout.LayoutFlags="PositionProportional" AbsoluteLayout.LayoutBounds="0.5,0.5" Command="{TemplateBinding MyCommand}" />
</AbsoluteLayout>
</Grid>
</ControlTemplate>
</ContentPage.ControlTemplate>
</ContentPage>
And here's the C# for that BasePage:
using System;
using System.Windows.Input;
using ShoppingListNEW.Views;
using Xamarin.Forms;
namespace ShoppingListNEW.Pages
{
public partial class BasePage : ContentPage
{
public string MyTextLabel { get; set; } = "This Works";
public ICommand MyCommand { get; set; }
public BasePage()
{
InitializeComponent();
MyCommand = new Command(() =>
{
Console.Write("But this doesn't work :(");
});
}
public void ShowHideLoading(bool showhide, bool allowCancel = false)
{
var loadingLayout = (AbsoluteLayout)GetTemplateChild("loading");
var cancelButton = loadingLayout.FindByName<Button>("btnCancel");
cancelButton.IsVisible = allowCancel;
loadingLayout.IsVisible = showhide;
}
}
}
Any ideas?
On your ControlTemplate binding, please use the following code:
<Button
AbsoluteLayout.LayoutBounds="0.5,0.5"
AbsoluteLayout.LayoutFlags="PositionProportional"
Command="{TemplateBinding BindingContext.MyCommand}"
IsVisible="True"
Text="{TemplateBinding BindingContext.MyTextLabel}" />
I use your code to create simple that you can take a look, please don't set AbsoluteLayout IsVisible="False".
<ContentPage.ControlTemplate>
<ControlTemplate>
<Grid Padding="0">
<StackLayout>
<ScrollView VerticalOptions="FillAndExpand">
<StackLayout
Orientation="Vertical"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToParent,
Property=Height}"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToParent,
Property=Width}">
<ContentPresenter />
</StackLayout>
</ScrollView>
</StackLayout>
<AbsoluteLayout
x:Name="loading"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All"
BackgroundColor="#85000000">
<ActivityIndicator
AbsoluteLayout.LayoutBounds="0.5,0.4"
AbsoluteLayout.LayoutFlags="PositionProportional"
IsEnabled="true"
IsRunning="true"
IsVisible="true"
Scale="2"
Color="Lime" />
<Button
AbsoluteLayout.LayoutBounds="0.5,0.5"
AbsoluteLayout.LayoutFlags="PositionProportional"
Command="{TemplateBinding BindingContext.MyCommand}"
IsVisible="True"
Text="{TemplateBinding BindingContext.MyTextLabel}" />
</AbsoluteLayout>
</Grid>
</ControlTemplate>
</ContentPage.ControlTemplate>
public partial class Page19 : ContentPage
{
public Page19()
{
InitializeComponent();
this.BindingContext = new Viewmodel1();
}
}
public class Viewmodel1:ViewModelBase
{
private string _MyTextLabel;
public string MyTextLabel
{
get { return _MyTextLabel; }
set
{
_MyTextLabel = value;
RaisePropertyChanged("MyTextLabel");
}
}
public RelayCommand MyCommand { get; set; }
public Viewmodel1()
{
MyCommand = new RelayCommand(method);
MyTextLabel = "this is test";
}
private void method()
{
Console.WriteLine("this is test!");
}
}
Problem solved. Thanks to Mikolaj Kieres for the answer, it was a simple case of initialising the Command earlier so I moved it up above the InitializeComponent and that kicked it into life. Thanks!
I am currently doing a test with ICommand and I wonder why ICommand does not fire with button in ListBox.ItemTemplate. But when used outside the template, it works.
here's the window xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1" x:Class="WpfApplication1.Window2"
Title="Window2" Height="300" Width="300">
<Window.DataContext>
<local:W2VM/>
</Window.DataContext>
<Grid>
<ListBox x:Name="listHistory" BorderThickness="0" Margin="0" Padding="0" HorizontalContentAlignment="Stretch" ItemsSource="{Binding History}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding ''}" />
<Button
Grid.Column="1"
HorizontalAlignment="Right"
x:Uid="btnDeleteHistoryItem"
x:Name="btnDeleteHistoryItem"
Content="r"
FontFamily="Marlett"
Visibility="Hidden" Command="{Binding MeClick}"
/>
</Grid>
<DataTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Visibility" TargetName="btnDeleteHistoryItem" Value="Visible" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content=" exit " VerticalAlignment="Bottom" Command="{Binding ExitCommand}" />
</Grid>
</Window>
here's the complete ViewModel code
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window2.xaml
/// </summary>
public partial class Window2 : Window
{
public Window2()
{
InitializeComponent();
}
}
public class W2VM : ViewModelBase
{
public List<string> _History = new List<string>();
public List<string> History
{
get { return this._History; }
}
public ICommand MeClick
{
get;
internal set;
}
public ICommand ExitCommand
{
get;
internal set;
}
public W2VM()
{
this.History.AddRange(new string[] {
"jayson", "hello", "world"
});
this.MeClick = new RelayCommand(Test);
this.ExitCommand = new RelayCommand(Exit);
}
void Exit()
{
Application.Current.Shutdown();
}
void Test()
{
Debug.WriteLine("hello world");
MessageBox.Show("do something incredible");
}
}
}
Test() does not fire
ok I got it working..
<Window.Resources>
<me:W2VM x:Key="local" />
</Window.Resources>
instead of
<Window.DataContext>
<local:W2VM/>
</Window.DataContext>
and my window grid
<Grid DataContext="{StaticResource local}">
<ListBox x:Name="listHistory" BorderThickness="0" Margin="0" Padding="0" HorizontalContentAlignment="Stretch" ItemsSource="{Binding History}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding ''}" />
<Button
Grid.Column="1"
HorizontalAlignment="Right"
x:Uid="btnDeleteHistoryItem"
x:Name="btnDeleteHistoryItem"
Content="r"
FontFamily="Marlett"
Visibility="Hidden" Command="{Binding MeClick, Source={StaticResource local}}" />
</Grid>
<DataTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Visibility" TargetName="btnDeleteHistoryItem" Value="Visible" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content=" exit " VerticalAlignment="Bottom" Command="{Binding ExitCommand}" />
</Grid>
now this will raise another question.
How would I know which ListViewItem was clicked when the btnDeleteHistoryItem command was just routed?
Are you using the relaycommand of mvvm-light?
I use command parameters to pass information to the command method:
<Button
Grid.Column="1"
HorizontalAlignment="Right"
x:Uid="btnDeleteHistoryItem"
x:Name="btnDeleteHistoryItem"
Content="r"
FontFamily="Marlett"
Visibility="Hidden" Command="{Binding MeClick, Source=StaticResource local}}"
CommandParameter="YourUniqueIdentifyingVariable" />
and then in your view model:
public RelayCommand<string> MeClick{
get { return _MyClick; }
private set { _MyClick = value; }
}
public New()
{
MeClick = new RelayCommand<string>(MySub, MySubIsEnabled);
}
private void MySub(string sParam)
{
//do stuff here, YourUniqueIdentifyingVariable is sParam
}
private bool MySubIsEnabled(string sParam)
{
return true;
}
you can bind "YourUniqueIdentifyingVariable" to the ID of the record or some other value that you can process in your command method
Code is untested but should work
I am walking through Windows Store Sample Application XAML Twitter Client 1, to get the same features in my own application. But i can't get binding work in 1 to 1 sample page.
This is my grid for displaying friends:
<GridView x:Name="FriendsGrid"
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10,10,0,0"
ItemsSource="{Binding Friends}"
ItemTemplate="{StaticResource FriendItemTemplate}"
Grid.ColumnSpan="2">
<GridView.DataContext>
<Model:FriendsViewModel/>
</GridView.DataContext>
Template for binding:
<Page.Resources>
<DataTemplate x:Key="FriendItemTemplate">
<Grid Height="200" Width="300">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image HorizontalAlignment="Left"
Height="80" Width="130"
Margin="10,10,0,0"
VerticalAlignment="Top"
Source="{Binding RealPhoto}"
Stretch="UniformToFill"/>
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
Margin="10,10,0,0"
TextWrapping="Wrap"
VerticalAlignment="Top"
Height="80"
Width="130"
Text="{Binding FirstName}"/>
<TextBlock HorizontalAlignment="Left"
Margin="10,10,0,0"
Grid.Row="1"
TextWrapping="Wrap"
Text="{Binding LastName}"
VerticalAlignment="Top"
Width="280" Height="80"
Grid.ColumnSpan="2" />
</Grid>
</DataTemplate>
</Page.Resources>
In my code-behind file:
private FriendsViewModel _model;
public MyPage()
{
this.InitializeComponent();
_model = new FriendsViewModel();
FriendsGrid.DataContext = _model;
}
Than i populate model, in application i see exactly the same items count that i`ve added, but items are empty. Using debug i see, that model is not empty.
Also when i am hard-coding values in templates, they are visible.
Test project on GitHub
I forgot to add getters and setters to properties of my model. Initially i had this:
public double Uid;
Than i added {get; set;}
[DataContract]
public class Friend
{
[DataMember(Name = "uid")]
public double Uid { get; set; }
[DataMember(Name="first_name")]
public string FirstName { get; set; }
[DataMember(Name="last_name")]
public string LastName { get; set; }
[DataMember(Name="online")]
public bool Online { get; set; }
[DataMember(Name = "photo")]
public string Photo { get; set; }
public ImageSource RealPhoto { get; set; }
}
To see changes in your view model you need to implement INotifyPropertyChanged. Part of doing this is calling NotifyPropertyChanged on a property if its value changes.
I have been searching all over the web trying to find an example of databinding an Accordion control.
I have written a simple test app to try and databind, and I can get the headers to bind, but can't seem to figure out how to get the content to bind. Could someone help me out?
Here is my XAML:
<tk:Accordion HorizontalAlignment="Left" Margin="12,12,0,0" Name="accordion1" Width="181" Height="325" Background="White" VerticalAlignment="Top">
<tk:Accordion.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding MenuHeaderName}" />
</StackPanel>
</DataTemplate>
</tk:Accordion.ItemTemplate>
<tk:Accordion.ContentTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=MenuItems.MenuItemName}" />
</StackPanel>
</DataTemplate>
</tk:Accordion.ContentTemplate>
</tk:Accordion>
And here is my code behind:
public partial class MainPage : UserControl
{
public class MenuItem
{
public MenuItem(string name) { MenuItemName = name; }
public string MenuItemName { get; set; }
}
public class MenuHeader
{
public MenuHeader(string name)
{
MenuItems = new List<MenuItem>();
MenuHeaderName = name;
}
public string MenuHeaderName { get; set; }
public List<MenuItem> MenuItems { get; set; }
}
public MainPage()
{
InitializeComponent();
List<MenuHeader> menuHeaders = new List<MenuHeader>();
MenuHeader robots = new MenuHeader("Robots");
robots.MenuItems.Add(new MenuItem("Robots - Item 1"));
robots.MenuItems.Add(new MenuItem("Robots - Item 2"));
robots.MenuItems.Add(new MenuItem("Robots - Item 3"));
menuHeaders.Add(robots);
MenuHeader pirates = new MenuHeader("Pirates");
pirates.MenuItems.Add(new MenuItem("Pirates - Item 1"));
pirates.MenuItems.Add(new MenuItem("Pirates - Item 2"));
pirates.MenuItems.Add(new MenuItem("Pirates - Item 3"));
menuHeaders.Add(pirates);
accordion1.ItemsSource = menuHeaders;
}
}
Figured it out.
Here is the XAML that works...
<tk:Accordion HorizontalAlignment="Left" Margin="12,12,0,0" Name="accordion1" Width="181" Height="325" Background="White" VerticalAlignment="Top">
<tk:Accordion.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding MenuHeaderName}" />
</StackPanel>
</DataTemplate>
</tk:Accordion.ItemTemplate>
<tk:Accordion.ContentTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding MenuItems}" BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding MenuItemName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</tk:Accordion.ContentTemplate>
</tk:Accordion>