I want to bind a CustomLabel to a VM by creating a new bindable property.
In OneWay mode, the first VM data has properly changed the property of the CustomLabel. but It didn't work from second time.
Although The VM event has occur, the Bindable Property of CustomView has not fired its PropertyChanged event.
It works properly in TwoWay mode though.
I've been testing for two days and searching for the cause, but I coudn't find it well.
Anybody tell me how to do?
// HomeViewModel.cs
public class HomeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _customName = "-";
public string CustomName
{
get
{
Debug.WriteLine("Get_CustomName");
return _customName;
}
set
{
if (value != _customName)
{
Debug.WriteLine("Set_CustomName");
_customName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomName)));
}
}
}
}
// MainPage.cs
public partial class MainPage : ContentPage
{
HomeViewModel Vm = new HomeViewModel();
public MainPage()
{
InitializeComponent();
BindingContext = Vm;
}
void ButtonTrue_Clicked(object sender, EventArgs e)
{
Vm.CustomName = "True";
}
void ButtonFalse_Clicked(object sender, EventArgs e)
{
Vm.CustomName = "False";
}
}
<!-- MainPage.xaml -->
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Ex_Binding"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Ex_Binding.MainPage">
<StackLayout Padding="50,0" VerticalOptions="Center">
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<Label Text="Custom Result : " />
<local:CustomLabel x:Name="lbCustom" MyText="{Binding CustomName}" HorizontalOptions="Center" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<Button Text="TRUE" BackgroundColor="LightBlue" HorizontalOptions="FillAndExpand" Clicked="ButtonTrue_Clicked" />
<Button Text="FALSE" BackgroundColor="LightPink" HorizontalOptions="FillAndExpand" Clicked="ButtonFalse_Clicked" />
</StackLayout>
</StackLayout>
</ContentPage>
// CustomLabel.cs
public class CustomLabel : Label
{
public static readonly BindableProperty MyTextProperty = BindableProperty.Create(nameof(MyText), typeof(string), typeof(CustomLabel), null, BindingMode.OneWay, propertyChanged: OnMyTextChanged);
private static void OnMyTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisBindable = (CustomLabel)bindable;
if (thisBindable != null)
{
thisBindable.MyText = (string)newValue;
}
}
public string MyText
{
get => (string)GetValue(MyTextProperty);
set
{
SetValue(MyTextProperty, value);
Text = value;
}
}
}
Cause :
thisBindable.MyText = (string)newValue;
Because you set the value of MyText when its value changed . So it will never been invoked next time (in TwoWay the method will been invoked multi times).
Solution:
You should set the Text in OnMyTextChanged directly .
private static void OnMyTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisBindable = (CustomLabel)bindable;
if (thisBindable != null)
{
thisBindable.Text = (string)newValue;
}
}
public string MyText
{
get => (string)GetValue(MyTextProperty);
set
{
SetValue(MyTextProperty, value);
//Text = value;
}
}
Related
I have a collectionview that is bound to an ObservableRangeCollectionin my ViewModel.
In my ViewModel there is a Method that runs onAppearing and I want my ColletionViewto be filled from there, but when I do so the collectionveiw dose not display the content only when i reload the content is shown.
View:
<RefreshView Grid.Row="1"
Grid.RowSpan="2"
Command="{Binding RefreshCommand}"
IsRefreshing="{Binding IsBusy, Mode=OneWay}">
<RefreshView.RefreshColor>
<OnPlatform x:TypeArguments="Color">
<On Platform="iOS" Value="White"/>
</OnPlatform>
</RefreshView.RefreshColor>
<CollectionView x:Name="Collection"
ItemsSource="{Binding Locations, Mode=OneWay}"
ItemTemplate="{StaticResource ListDataTemplate}"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}"
RemainingItemsThreshold="10"
SelectionMode="Single"
BackgroundColor="Transparent"
ItemsLayout="VerticalList"
SelectedItem="{Binding SelectedItem}"
SelectionChangedCommand="{Binding SelectedCommand}">
<CollectionView.EmptyView>
<StackLayout Padding="12">
<Label HorizontalOptions="Center" Text="Keine Daten vorhanden!" TextColor="White"/>
</StackLayout>
</CollectionView.EmptyView>
</CollectionView>
</RefreshView>
ViewModel:
namespace YourPartys.ViewModels
{
public class ListViewModel : ViewModelBase
{
#region Variables
#endregion
#region Propertys
LocationModel selectedItem;
public LocationModel SelectedItem
{
get => selectedItem;
set => SetProperty(ref selectedItem, value);
}
public ObservableRangeCollection<LocationModel> Locations { get;set; } = new ObservableRangeCollection<LocationModel>();
double distance;
public double Distance
{
get => distance;
set => SetProperty(ref distance, value);
}
#endregion
#region Commands
public ICommand FilterButtonCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand SelectedCommand { get; }
public ICommand LoadMoreCommand { get; }
#endregion
//Constructor
public ListViewModel()
{
FilterButtonCommand = new Command(OpenFilter);
RefreshCommand = new AsyncCommand(Refresh);
SelectedCommand = new AsyncCommand(Select);
}
public override async void VModelActive(Page sender, EventArgs eventArgs)
{
base.VModelActive(sender, eventArgs);
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
}
private void OpenFilter(object obj)
{
PopupNavigation.Instance.PushAsync(new ListFilterPage());
}
private async Task Refresh()
{
IsBusy = true;
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
IsBusy = false;
}
private async Task Select()
{
if (SelectedItem == null)
return;
var route = $"{nameof(DetailPage)}?Locationid={SelectedItem.Locationid}";
SelectedItem = null;
await AppShell.Current.GoToAsync(route);
}
}
}
There are several problems in your demo.
1.Since you set the BindingContext for your page in xaml as follows:
<ContentPage.BindingContext>
<viewmodels:MainViewModel/>
</ContentPage.BindingContext>
you didn't need to recreate another object MainViewModel in a CS file and reference it. These are two different objects.
MainViewModel viewModel;
viewModel = new MainViewModel();
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.VModelActive(this, EventArgs.Empty);
}
So, you can get the BindingContext in MainPage.xaml.cs in function OnAppearing as follows:
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
The whole code is
public partial class MainPage : ContentPage
{
MainViewModel viewModel;
public MainPage()
{
InitializeComponent();
// viewModel = new MainViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
}
2.when we set the text color of the Label to White,this makes it hard to see the text,so you can reset it to another color,for example Black:
<Label Text="{Binding Name}"
FontSize="30"
TextColor="White"/>
Please check the GIF for the problem.
I am actually using two imagebutton here and change IsVisible, since I couldn't accomplish swapping the image by Binding on the source.
ViewModel:
public bool IsAudioPlaying
{
get => player.IsPlaying;
}
...
public void PlayOrPause()
{
if (player.IsPlaying)
player.Pause();
else
player.Play();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsAudioPlaying"));
}
XAML:
<ImageButton AbsoluteLayout.LayoutBounds=".5, 0, 100, 100" AbsoluteLayout.LayoutFlags="PositionProportional" Source="play.png" Padding="20" WidthRequest="80" HeightRequest="80"
CornerRadius="40" VerticalOptions="Center" HorizontalOptions ="Center" BackgroundColor="#cea448" Clicked="PlayOrPause" Margin="10" IsVisible="{Binding IsAudioPlaying, Converter={StaticResource InverseBoolConverter}}" />
<ImageButton AbsoluteLayout.LayoutBounds=".5, 0, 100, 100" AbsoluteLayout.LayoutFlags="PositionProportional" Source="pause.png" Padding="20" WidthRequest="80" HeightRequest="80"
CornerRadius="40" VerticalOptions="Center" HorizontalOptions ="Center" BackgroundColor="#cea448" Clicked="PlayOrPause" Margin="10" IsVisible="{Binding IsAudioPlaying}" />
I am actually using two imagebutton here and change IsVisible, since I couldn't accomplish swapping the image by Binding on the source.
Create a View Model.
public class ViewModel : INotifyPropertyChanged
{
private bool _isAudioPlaying;
public bool IsAudioPlaying
{
get
{
return _isAudioPlaying;
}
set
{
_isAudioPlaying = value;
OnPropertyChanged("IsAudioPlaying");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Use ObservableCollection to update.
ObservableCollection<ViewModel> observableCollection { get; set; }
public MainPage()
{
InitializeComponent();
observableCollection = new ObservableCollection<ViewModel>()
{
new ViewModel(){ IsAudioPlaying=true}
};
this.BindingContext = observableCollection;
}
private void PlayOrPause(object sender, EventArgs e)
{
if (observableCollection[0].IsAudioPlaying == true)
{
observableCollection[0].IsAudioPlaying = false;
imageButton.Source = "pause.png";
}
else
{
observableCollection[0].IsAudioPlaying = true;
imageButton.Source = "play.png";
}
}
I have this template:
<?xml version="1.0" encoding="utf-8"?>
<Grid Padding="20,0" xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Japanese;assembly=Japanese"
x:Class="Japanese.Templates.DataGridTemplate"
x:Name="this" HeightRequest="49" Margin="0">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding TapCommand, Source={x:Reference this}}"
CommandParameter="1"
NumberOfTapsRequired="1" />
</Grid.GestureRecognizers>
<Label Grid.Column="0" Text="{Binding Test" />
</Grid>
Behind this I have:
public partial class DataGridTemplate : Grid
{
public DataGridTemplate()
{
InitializeComponent();
}
public static readonly BindableProperty TapCommandProperty =
BindableProperty.Create(
"Command",
typeof(ICommand),
typeof(DataGridTemplate),
null);
public ICommand TapCommand
{
get { return (ICommand)GetValue(TapCommandProperty); }
set { SetValue(TapCommandProperty, value); }
}
}
and I am trying to call the template like this in file: Settings.xaml.cs
<template:DataGridTemplate TapCommand="openCFSPage" />
hoping that it will call my method here in file: Settings.cs
void openCFSPage(object sender, EventArgs e)
{
Navigation.PushAsync(new CFSPage());
}
The code compiles but when I click on the grid it doesn't call the openCFSPage method.
1) Does anyone have an idea what might be wrong?
2) Also is there a way that I can add a parameter to the template and then have that parameter passed to my method in the CS back end code?
Note that I would like to avoid adding a view model if possible. The application is small and I'd like to just have the code I need in the CS code of the page that calls the template.
Please note that the simplest way to implement this would be through MVVM (i.e. a view-model), but if you want to side-step this option (as you mentioned in the question) then you can use one of the following options
Option1 : Wrap delegate into command object
If you look at it from the perspective of a XAML parser, you are technically trying to assign a delegate to a property of type ICommand. One way to avoid the type mismatch would be to wrap the delegate inside a command-property in the page's code-behind.
Code-behind [Settings.xaml.cs]
ICommand _openCFSPageCmd;
public ICommand OpenCFSPageCommand {
get {
return _openCFSPageCmd ?? (_openCFSPageCmd = new Command(OpenCFSPage));
}
}
void OpenCFSPage(object param)
{
Console.WriteLine($"Control was tapped with parameter: {param}");
}
XAML [Settings.xaml]
<!-- assuming that you have added x:Name="_parent" in root tag -->
<local:DataGridView TapCommand="{Binding OpenCFSPageCommand, Source={x:Reference _parent}}" />
Option2 : Custom markup-extension
Another option (a bit less mainstream) is to create a markup-extension that wraps the delegate into a command object.
[ContentProperty("Handler")]
public class ToCommandExtension : IMarkupExtension
{
public string Handler { get; set; }
public object Source { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
throw new ArgumentNullException(nameof(serviceProvider));
var lineInfo = (serviceProvider?.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo ?? new XmlLineInfo();
object rootObj = Source;
if (rootObj == null)
{
var rootProvider = serviceProvider.GetService<IRootObjectProvider>();
if (rootProvider != null)
rootObj = rootProvider.RootObject;
}
if(rootObj == null)
{
var valueProvider = serviceProvider.GetService<IProvideValueTarget>();
if (valueProvider == null)
throw new ArgumentException("serviceProvider does not provide an IProvideValueTarget");
//we assume valueProvider also implements IProvideParentValues
var propInfo = valueProvider.GetType()
.GetProperty("Xamarin.Forms.Xaml.IProvideParentValues.ParentObjects",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if(propInfo == null)
throw new ArgumentException("valueProvider does not provide an ParentObjects");
var parentObjects = propInfo.GetValue(valueProvider) as IEnumerable<object>;
rootObj = parentObjects?.LastOrDefault();
}
if(rootObj != null)
{
var delegateInfo = rootObj.GetType().GetMethod(Handler,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if(delegateInfo != null)
{
var handler = Delegate.CreateDelegate(typeof(Action<object>), rootObj, delegateInfo) as Action<object>;
return new Command((param) => handler(param));
}
}
throw new XamlParseException($"Can not find the delegate referenced by `{Handler}` on `{Source?.GetType()}`", lineInfo);
}
}
Sample usage
<local:DataGridView TapCommand="{local:ToCommand OpenCFSPage}" />
You have 2 options depending on the the use case :
FYI, there's no way to call another method directly from the view (its a bad design pattern to do so)
Using Event Aggregator :
Create interface
public interface IEventAggregator
{
TEventType GetEvent<TEventType>() where TEventType : EventBase, new();
}
All you have to do is call it from you TapCommand
_eventAggregator.GetEvent<ItemSelectedEvent>().Publish(_selectedItem);
Then in your Settings.cs you can Create a method that can receive the data
this.DataContext = new ListViewModel(ApplicationService.Instance.EventAggregator);
Inheritance and Polymorphism / Making openCFSPage a service :
Creating a interface / service that links both models
public interface IOpenCFSPage
{
Task OpenPage();
}
and a method :
public class OpenCFSPage : IOpenCFSPage
{
private INavigationService _navigationService;
public OpenCFSPage(INavigationService navigationService){
_navigationService = navigationService;
}
public async Task OpenPage()
{
await _navigationService.NavigateAsync(new CFSPage());
}
}
Settings.xaml:
<template:DataGridTemplate TapCommand="{Binding OpenCFSPage}" />
<!-- Uncomment below and corresponding parameter property code in DataGridTemplate.xaml.cs to pass parameter from Settings.xaml -->
<!--<template:DataGridTemplate TapCommand="{Binding OpenCFSPage}" CommandParameter="A" />-->
Settings.xaml.cs:
public Settings()
{
InitializeComponent();
OpenCFSPage = new Command(p => OpenCFSPageExecute(p));
BindingContext = this;
}
public ICommand OpenCFSPage { get; private set; }
void OpenCFSPageExecute(object p)
{
var s = p as string;
Debug.WriteLine($"OpenCFSPage:{s}:");
}
DataGridTemplate.xaml:
<?xml version="1.0" encoding="UTF-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Japanese;assembly=Japanese"
Padding="0,20"
HeightRequest="49" Margin="0"
x:Class="Japanese.DataGridTemplate">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding TapCommand}"
CommandParameter="1"
NumberOfTapsRequired="1" />
</Grid.GestureRecognizers>
<Label Grid.Column="0" Text="Test" />
</Grid>
DataGridTemplate.xaml.cs:
public partial class DataGridTemplate : Grid
{
public DataGridTemplate()
{
InitializeComponent();
}
public static readonly BindableProperty TapCommandProperty =
BindableProperty.Create(
nameof(TapCommand), typeof(ICommand), typeof(DataGridTemplate), null,
propertyChanged: OnCommandPropertyChanged);
public ICommand TapCommand
{
get { return (ICommand)GetValue(TapCommandProperty); }
set { SetValue(TapCommandProperty, value); }
}
//public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
// nameof(CommandParameter), typeof(string), typeof(DataGridTemplate), null);
//public string CommandParameter
//{
// get { return (string)GetValue(CommandParameterProperty); }
// set { SetValue(CommandParameterProperty, value); }
//}
static TapGestureRecognizer GetTapGestureRecognizer(DataGridTemplate view)
{
var enumerator = view.GestureRecognizers.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
if (item is TapGestureRecognizer) return item as TapGestureRecognizer;
}
return null;
}
static void OnCommandPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is DataGridTemplate view)
{
var tapGestureRecognizer = GetTapGestureRecognizer(view);
if (tapGestureRecognizer != null)
{
tapGestureRecognizer.Command = (ICommand)view.GetValue(TapCommandProperty);
//tapGestureRecognizer.CommandParameter = (string)view.GetValue(CommandParameterProperty);
}
}
}
}
Check this code you help you. Here you have to pass a reference of list view and also you need to bind a command with BindingContext.
<ListView ItemsSource="{Binding Sites}" x:Name="lstSale">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical">
<Label Text="{Binding FriendlyName}" />
<Button Text="{Binding Name}"
HorizontalOptions="Center"
VerticalOptions="Center"
Command="{Binding
Path=BindingContext.RoomClickCommand,
Source={x:Reference lstSale}}"
CommandParameter="{Binding .}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I Am working with a ListView Control in XF application. My XAML Code looks like this.
<ListView ItemsSource="{Binding RechargeList}" HasUnevenRows="True" VerticalOptions="FillAndExpand">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Path=SelectedParkingID}" TextColor="Red" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
While my code behind looks like
private ObservableCollection<Recharge> _RechargeList = new ObservableCollection<Recharge>();
public ObservableCollection<Recharge> RechargeList
{
get
{
return _RechargeList;
}
set
{
SetProperty(ref _RechargeList, value);
}
}
And I add Items to Collection in DelegateCommand Event
RechargeList.Add(new Recharge() { SelectedParkingIDParkingID = ParkingID, RechargeAmount = double.Parse(RechargeAmount), BalanceAmount = 10 });
However, the Listview fails to refresh. Could some one help me ?
Looks like you have a typo
<TextCell Text="{Binding Path=SelectedParkingID}" TextColor="Red" />
Should be
<TextCell Text="{Binding Path=SelectedParkingIDParkingID }" TextColor="Red" />
based on what your model looks like. If you try to bind to a property that doesn't exist, it fails softly. So you're adding an item, but the TextCell doesn't render since it has no content.
Please try to implement INotifyPropertyChanged interface in your class.
public class Data : INotifyPropertyChanged
{
// boiler-plate
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
// props
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
}
Each property is then just something like:
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
I want multi select drop down list for my mvvm cross UWP app. So is there any predefined control? Or I need to implement custom control to achieve this.
Any help or suggestions would be appreciated.
Thank You
Surprised that such common scenario is still not supported by built-in UWP ComboBox and that I couldn't found any working solutions anywhere.
Here is my minimal solution in case anyone else is looking.
XAML:
<UserControl
x:Class="MonoTorrent.GUI.Controls.MultiSelectComboBox"
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"
d:DesignHeight="300"
d:DesignWidth="400">
<StackPanel x:Name="rootElement" Orientation="Vertical" Margin="0">
<Button x:Name="comboBoxButton" BorderBrush="Gray" BorderThickness="2" Background="Transparent"
VerticalAlignment="Top" Click="ComboBoxButton_Click"
HorizontalAlignment="Stretch" FontSize="14" MinHeight="26" Height="26" Padding="0"
Width="{Binding ElementName=rootElement, Path=ActualWidth}">
<Grid VerticalAlignment="Stretch" Width="{Binding ElementName=rootElement, Path=ActualWidth}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="32" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="SelectedValueTextBlock"
Grid.Column="0" VerticalAlignment="Center" FontSize="15" HorizontalAlignment="Left" Padding="7,0,0,0" />
<FontIcon Grid.Column="1" FontSize="12" FontFamily="Segoe MDL2 Assets" Glyph="" HorizontalAlignment="Right"
Margin="0,5,10,5" VerticalAlignment="Center" />
</Grid>
</Button>
<Popup x:Name="comboBoxPopup" IsLightDismissEnabled="True">
<Border BorderBrush="{ThemeResource ComboBoxDropDownBorderBrush}"
BorderThickness="{ThemeResource ComboBoxDropdownBorderThickness}"
Background="{ThemeResource ComboBoxDropDownBackground}"
HorizontalAlignment="Stretch">
<ListView x:Name="listView"
SelectionMode="Multiple"
SingleSelectionFollowsFocus="False"
SelectionChanged="ListView_SelectionChanged">
</ListView>
</Border>
</Popup>
</StackPanel>
</UserControl>
C#:
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace MonoTorrent.GUI.Controls
{
public sealed partial class MultiSelectComboBox : UserControl
{
#region ItemsSource dependency property
public object ItemsSource
{
get { return GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
listView.ItemsSource = value;
}
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(object), typeof(MultiSelectComboBox), new PropertyMetadata(new List<object>(), OnItemsSourcePropertyChanged));
private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox instance = d as MultiSelectComboBox;
if (instance != null && e.NewValue != null)
{
instance.listView.ItemsSource = e.NewValue;
}
}
#endregion
#region ItemTemplate dependency property
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set
{
SetValue(ItemTemplateProperty, value);
listView.ItemTemplate = value;
}
}
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(MultiSelectComboBox), new PropertyMetadata(null, OnItemTemplatePropertyChanged));
private static void OnItemTemplatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox instance = d as MultiSelectComboBox;
if (instance != null && e.NewValue as DataTemplate != null)
{
instance.listView.ItemTemplate = (DataTemplate)e.NewValue;
}
}
#endregion
#region SelectedItems dependency property
public IList<object> SelectedItems
{
get { return (IList<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IList<object>), typeof(MultiSelectComboBox), new PropertyMetadata(new List<object>()));
#endregion
#region PopupHeight dependency property
public double PopupHeight
{
get { return (double)GetValue(PopupHeightProperty); }
set
{
SetValue(PopupHeightProperty, value);
if (value != 0)
{
listView.Height = value;
}
}
}
public static readonly DependencyProperty PopupHeightProperty =
DependencyProperty.Register("PopupHeight", typeof(double), typeof(MultiSelectComboBox), new PropertyMetadata(0.0, OnPopupHeightPropertyChanged));
private static void OnPopupHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox instance = d as MultiSelectComboBox;
if (instance != null && (double)e.NewValue != 0)
{
instance.listView.Height = (double)e.NewValue;
}
}
#endregion
#region PopupWidth dependency property
public double PopupWidth
{
get { return (double)GetValue(PopupWidthProperty); }
set
{
SetValue(PopupWidthProperty, value);
if (value != 0)
{
listView.Width = value;
}
}
}
public static readonly DependencyProperty PopupWidthProperty =
DependencyProperty.Register("PopupWidth", typeof(double), typeof(MultiSelectComboBox), new PropertyMetadata(0.0, OnPopupWidthPropertyChanged));
private static void OnPopupWidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox instance = d as MultiSelectComboBox;
if (instance != null && (double)e.NewValue != 0)
{
instance.listView.Width = (double)e.NewValue;
}
}
#endregion
#region NoSelectionText dependency property
public string NoSelectionText
{
get { return (string)GetValue(NoSelectionTextProperty); }
set { SetValue(NoSelectionTextProperty, value); }
}
public static readonly DependencyProperty NoSelectionTextProperty =
DependencyProperty.Register("NoSelectionText", typeof(string), typeof(MultiSelectComboBox), new PropertyMetadata("No selection"));
#endregion
#region MultipleSelectionTextFormat dependency property
public string MultipleSelectionTextFormat
{
get { return (string)GetValue(MultipleSelectionTextFormatProperty); }
set { SetValue(MultipleSelectionTextFormatProperty, value); }
}
public static readonly DependencyProperty MultipleSelectionTextFormatProperty =
DependencyProperty.Register("MultipleSelectionTextFormat", typeof(string), typeof(MultiSelectComboBox), new PropertyMetadata("{0} selected"));
#endregion
public MultiSelectComboBox()
{
this.InitializeComponent();
this.Loaded += MultiSelectComboBox_Loaded;
}
private void MultiSelectComboBox_Loaded(object sender, RoutedEventArgs e)
{
this.UpdateSelectionText();
}
private void ComboBoxButton_Click(object sender, RoutedEventArgs e)
{
listView.SelectedItems.Clear();
foreach (var item in SelectedItems)
{
listView.SelectedItems.Add(item);
}
this.comboBoxPopup.IsOpen = true;
}
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!comboBoxPopup.IsOpen)
{
return;
}
this.SelectedItems = listView.SelectedItems.ToList();
UpdateSelectionText();
}
private void UpdateSelectionText()
{
if (this.SelectedItems == null || this.SelectedItems.Count == 0)
{
this.SelectedValueTextBlock.Text = NoSelectionText;
}
else if (this.SelectedItems.Count == 1)
{
this.SelectedValueTextBlock.Text = this.SelectedItems.First().ToString();
}
else
{
this.SelectedValueTextBlock.Text = String.Format(MultipleSelectionTextFormat, this.SelectedItems.Count);
}
}
}
}
Usage:
<controls:MultiSelectComboBox x:Name="MultiSelectComboBox"
ItemsSource="{Binding Values}"
SelectedItems="{Binding SelectedValues, Mode=TwoWay}"
NoSelectionText="{Binding EmptySelectionString}"
MultipleSelectionTextFormat="{Binding MultipleSelectedFormatString}"
PopupHeight="500" PopupWidth="200"
HorizontalAlignment="Stretch" FontSize="14" MinHeight="26" Height="26">
<controls:MultiSelectComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</controls:MultiSelectComboBox.ItemTemplate>
</controls:MultiSelectComboBox>
Notes:
In current state can only be bound to a primitive type collection
like List< String >.
SelectedValues has to be List< object >, you can use
Cast< T > to convert from List< String >.
There is no built-in multi-select combobox in UWP, but you can build your own like this - Issue with multiselect combobox control in Windows 8 .
Basically you can add checkboxes to each item in a combobox and create a logic that will gather the selected items and provide a bindable way to access them.
To make it even simpler, you can create a special class that will have a IsChecked property and just add the checkbox with two-way binding to this property. This will ensure checking of the box in the UI will be reflected in the class and you can then just enumerate all the items to find those that have IsChecked set to true.
I would build it something like this...
First build your class
public class MultiSelectComboBox : ComboBox
{
public List<ComboBoxItem> SelectedItems
{
get { return (List<ComboBoxItem>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(List<ComboBoxItem>), typeof(MultiSelectComboBox), new PropertyMetadata(new List<ComboBoxItem>()));
public static void SetIsSelected(UIElement element, bool value)
{
element.SetValue(IsSelectedProperty, value);
}
public static bool GetIsSelected(UIElement element)
{
return (bool)element.GetValue(IsSelectedProperty);
}
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.RegisterAttached("IsSelected", typeof(bool), typeof(ComboBoxItem), new PropertyMetadata(false, OnIsSelectedChanged));
public static void SetParentComboBox(UIElement element, MultiSelectComboBox value)
{
element.SetValue(ParentComboBoxProperty, value);
}
public static MultiSelectComboBox GetParentComboBox(UIElement element)
{
return (MultiSelectComboBox)element.GetValue(ParentComboBoxProperty);
}
public static readonly DependencyProperty ParentComboBoxProperty =
DependencyProperty.RegisterAttached("ParentComboBox", typeof(MultiSelectComboBox), typeof(MultiSelectComboBox), new PropertyMetadata(null));
protected override DependencyObject GetContainerForItemOverride()
{
ComboBoxItem comboBoxitem = new ComboBoxItem();
MultiSelectComboBox.SetParentComboBox(comboBoxitem, this);
return comboBoxitem;
}
private static void OnIsSelectedChanged(object comboBoxItem, DependencyPropertyChangedEventArgs args)
{
ComboBoxItem item = comboBoxItem as ComboBoxItem;
if (item != null)
{
MultiSelectComboBox parent = MultiSelectComboBox.GetParentComboBox(item);
if (MultiSelectComboBox.GetIsSelected(item))
{
parent.SelectedItems.Add(item);
}
else
{
parent.SelectedItems.Remove(item);
}
}
}
}
Then create your items template
<local:MultiSelectComboBox ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems, Mode=TwoWay}">
<local:MultiSelectComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding ItemContent}" IsChecked="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}, Path=IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</local:MultiSelectComboBox.ItemTemplate>
</local:MultiSelectComboBox>
However, this might take some massaging to get working. In the end you want to subclass Combobox and make it do some new stuff.