I trying to implement StateContainer by Patrick McCurley in my .NET MAUI application.
It works correctly when the ListView displayed for the first time.
But ListView is not displaying when state changes again until I swipe the screen.
When I add any view element (label, button, etc.) to the view that contains the ListView, it does not show up. But ListView displayed correctly when I move StateContainer to Grid with any other view elements. ListView does not display correctly if the Grid contains no other elements than the StateContainer.
I can't figure out what's the problem here. Grid with other view elements is not a solution for me, because my page should not contain any other elements whan the StateContainer.
Here is an example that reproduces the problem:
P.S. I'm sorry for a lot of code :) I don't know where the problem could be.
States.cs
public enum States
{
Loading,
Success
}
StateCondition.cs
[ContentProperty("Content")]
public class StateCondition : View
{
public object State { get; set; }
public View Content { get; set; }
}
StateContainer.cs
[ContentProperty("Conditions")]
public class StateContainer : ContentView
{
public List<StateCondition> Conditions { get; set; } = new();
public static readonly BindableProperty StateProperty =
BindableProperty.Create(nameof(State), typeof(object), typeof(StateContainer), null, BindingMode.Default, null, StateChanged);
private static void StateChanged(BindableObject bindable, object oldValue, object newValue)
{
var parent = bindable as StateContainer;
if (parent != null)
parent.ChooseStateProperty(newValue);
}
public object State
{
get { return GetValue(StateProperty); }
set { SetValue(StateProperty, value); }
}
private void ChooseStateProperty(object newValue)
{
if (Conditions == null && Conditions?.Count == 0) return;
var stateCondition = Conditions
.FirstOrDefault(condition =>
condition.State != null &&
condition.State.ToString().Equals(newValue.ToString()));
if (stateCondition == null) return;
Content = stateCondition.Content;
}
}
MainPage.xaml
<ContentPage ...>
<state:StateContainer State="{Binding State}">
<state:StateCondition State="Loading">
<StackLayout HorizontalOptions="Center" VerticalOptions="Center">
<ActivityIndicator IsRunning="True" />
<Label Text="Updating data..." />
</StackLayout>
</state:StateCondition>
<state:StateCondition State="Success">
<ListView ItemsSource="{Binding SomeData}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding . }" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</state:StateCondition>
</state:StateContainer>
</ContentPage>
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
private States _state;
private int[] _someData;
public MainPage()
{
InitializeComponent();
this.BindingContext = this;
SomeData = new[] { 1, 2, 3, 4, 5 };
State = States.Success;
// it can be executed from outside the page
_ = Task.Run(ExecuteSomeWorkAsync);
}
public States State
{
get => _state;
private set
{
if (_state != value)
{
_state = value;
OnPropertyChanged();
}
}
}
public int[] SomeData
{
get => _someData;
private set
{
if (_someData != value)
{
_someData = value;
OnPropertyChanged();
}
}
}
public async Task ExecuteSomeWorkAsync()
{
await Task.Delay(2000);
State = States.Loading;
await Task.Delay(2000);
// generate new data for displaying
Random rnd = new();
var data = Enumerable.Range(0, 5).Select(n => rnd.Next(0, 5)).ToArray();
SomeData = data;
State = States.Success;
}
}
I suspect Content = stateCondition.Content; won't update display correctly.
As an alternative solution, define public class StateContainer : StackLayout, and use IsVisible="True"/"False" on each child, to control what is shown. All the stateConditions continue to be children of stateContainer, but make only one visible at a time.
Related
I have a shared view for Add and Detail page. For some reason in the detail page, the view model won't binding to this child view (page come up blank as in NO populated value from the api service). Any ideas?
Debug this and there was a data coming from web api for both CategoryList as well as _activity.
How to debug this binding process?
ActivityView.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AthlosifyMobileApp.Views.ActivityView">
<StackLayout Spacing="12">
<Entry x:Name="txtName" Text="{Binding Name}" HeightRequest="40" BackgroundColor="White" Placeholder="Name" HorizontalOptions="FillAndExpand"/>
<Entry x:Name="txtNoOfMinutes" Keyboard="Numeric" Text="{Binding NoOfMinutes}" BackgroundColor="White" Placeholder="NoOfMinutes" HorizontalOptions="FillAndExpand"/>
<Entry x:Name="txtDescription" Text="{Binding Description}" HeightRequest="40" BackgroundColor="White" Placeholder="Description" HorizontalOptions="FillAndExpand"/>
<Picker ItemsSource="{Binding CategoryList}" ItemDisplayBinding="{Binding Name}" SelectedItem="{Binding SelectedCategory}"></Picker>
</StackLayout>
</ContentView>
ActivityView.xaml.cs
namespace AthlosifyMobileApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ActivityView : ContentView
{
public ActivityView ()
{
InitializeComponent ();
}
}
}
ActivityDetailViewModel.cs
namespace AthlosifyMobileApp.ViewModels
{
public class ActivityDetailViewModel : ActivityBaseViewModel
{
public ICommand DeleteCommand { get; private set; }
public ICommand UpdateCommand { get; private set; }
public ActivityDetailViewModel(INavigation navigation, int selectedActivityId)
{
_navigation = navigation;
_activityValidator = new ActivityValidator();
_activity = new Activity();
_activity.Id = selectedActivityId;
_apiService = new ApiService();
//DeleteCommand = new Command(async () => await HandleDeleteActivity());
UpdateCommand = new Command(async () => await UpdateActivity());
FetchActivityDetail();
FetchCategories();
}
async void FetchActivityDetail()
{
_activity = await _apiService.GetActivity(_activity.Id);
}
async void FetchCategories()
{
CategoryResult categoryResult = await _apiService.GetCategories();
CategoryList = categoryResult.Results;
}
async Task UpdateActivity()
{
_activity.OwnerId = Preferences.Get(Constant.Setting_UserId, "");
_activity.CategoryId = SelectedCategory.Id;
_activity.CategoryName = SelectedCategory.Name;
var validationResults = _activityValidator.Validate(_activity);
if (validationResults.IsValid)
{
bool isUserAccept = await Application.Current.MainPage.DisplayAlert("Contact Details", "Update Contact Details", "OK", "Cancel");
if (isUserAccept)
{
var response = await _apiService.UpdateActivity(_activity.Id,_activity);
if (!response)
{
await Application.Current.MainPage.DisplayAlert("Add Activity", "Error", "Alright");
}
else
{
await _navigation.PushAsync(new ActivityListPage());
}
await _navigation.PopAsync();
}
}
else
{
await Application.Current.MainPage.DisplayAlert("Add Contact", validationResults.Errors[0].ErrorMessage, "Ok");
}
}
public async Task HandleDeleteActivity(int id)
{
var alert = await Application.Current.MainPage.DisplayAlert("Warning", "Do you want to delete this item?", "Yes", "Cancel");
if (alert)
{
var response = await _apiService.DeleteActivity(id);
if (!response)
{
await Application.Current.MainPage.DisplayAlert("Error", "Something wrong", "Alright");
}
else
{
await _navigation.PushAsync(new ActivityListPage());
}
}
}
}
}
ActivityBaseViewModel.cs
namespace AthlosifyMobileApp.ViewModels
{
public class ActivityBaseViewModel : INotifyPropertyChanged
{
public Activity _activity;
public INavigation _navigation;
public IValidator _activityValidator;
public ApiService _apiService;
public string Name
{
get
{
return _activity.Name;
}
set
{
_activity.Name = value;
NotifyPropertyChanged("Name");
}
}
public string Description
{
get { return _activity.Description; }
set
{
_activity.Description = value;
NotifyPropertyChanged("Description");
}
}
public int NoOfMinutes
{
get { return _activity.NoOfMinutes; }
set
{
_activity.NoOfMinutes = value;
NotifyPropertyChanged("NoOfMinutes");
}
}
public int CategoryId
{
get { return _activity.CategoryId; }
set
{
_activity.CategoryId = value;
NotifyPropertyChanged("CategoryId");
}
}
public string CategoryName
{
get { return _activity.CategoryName; }
set
{
_activity.CategoryName = value;
NotifyPropertyChanged("CategoryName");
}
}
//List<Activity> _activityList;
InfiniteScrollCollection<Activity> _activityList;
//public List<Activity> ActivityList
public InfiniteScrollCollection<Activity> ActivityList
{
get => _activityList;
set
{
_activityList = value;
NotifyPropertyChanged("ActivityList");
}
}
List<Category> _categoryList;
public List<Category> CategoryList
{
get { return _categoryList; }
set
{
_categoryList = value;
NotifyPropertyChanged("CategoryList");
}
}
public Category SelectedCategory
{
get
{
return _activity.SelectedCategory;
}
set
{
_activity.SelectedCategory = value;
NotifyPropertyChanged("SelectedCategory");
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
ActivityDetailPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:AthlosifyMobileApp.Views"
x:Class="AthlosifyMobileApp.Views.ActivityDetailPage"
Title="Detail Activity">
<ContentPage.ToolbarItems>
<ToolbarItem Command="">
<ToolbarItem.IconImageSource>
<FontImageSource Glyph="" FontFamily="{StaticResource MaterialFontFamily}"/>
</ToolbarItem.IconImageSource>
</ToolbarItem>
<ToolbarItem Command="{Binding UpdateCommand}">
<ToolbarItem.IconImageSource>
<FontImageSource Size="30" Glyph="" FontFamily="{StaticResource MaterialFontFamily}"/>
</ToolbarItem.IconImageSource>
</ToolbarItem>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Padding="20" Spacing="12">
<local:ActivityView />
</StackLayout>
</ContentPage.Content>
</ContentPage>
ActivityDetailPage.xaml.cs
namespace AthlosifyMobileApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ActivityDetailPage : ContentPage
{
public ActivityDetailPage(int activityId)
{
InitializeComponent ();
BindingContext = new ActivityDetailViewModel(Navigation, activityId);
}
}
}
Based on the code you shared, I think it's likely you're not seeing any data on the activity detail page because you are fetching the data via an async method that is not awaited (FetchActivityData). Btw, async void method should be avoided if possible. There is no way to catch/handle exceptions thrown from them.
It looks like you are not awaiting because you are calling from the constructor of your viewmodel. What actually happens here is that the constructor returns immediately, while FetchActivityDetail() and FetchCategories() continue to run in the background. The page is displayed, but there is no data yet, so you don't see anything displayed. Then, when FetchActivityDetail completes, it sets _activity, but that's a field, so no PropertyChanged events are fired, so the page doesn't know it needs to update.
Here are a few suggestions:
Do not perform long-running processes (like fetching data) in constructors. Passing in the existing data (like your activityid), is generally ok, although it can make using dependency injection a bit harder, if you eventually want to do that.
When navigating to a viewmodel that requires fetching data, I generally recommend waiting until the view/vm are displayed before making the api call. To do this, I have all my views call an OnAppearing method in my viewmodels. This is easily moved into a BasePage and BaseViewModel that everything inherits from. Then, you can do things like setting an IsBusy status (to trigger some UI like a spinner), and populate your data. It could look something like this:
public override async Task OnAppearing()
{
await base.OnAppearing();
try
{
IsBusy = true;
await FetchActivityDetail();
await FetchCategories();
}
catch (Exception ex)
{
//handle/display error
}
finally
{
IsBusy = false;
}
}
Another option would be to make this a method that's called prior to navigation, but that would require creating the viewmodel first, which is a different navigation pattern than you're using here. There are some good examples out there of viewmodel-first navigation, but I won't go into that here.
Ensure that when data is fetched, it sets properties that cause PropertyChanged events to fire, so the view bindings update. You can't just set a backing field.
According to your description, you want to bind custom view in Xamarin.Forms, I suggest you don't assign binding internally inside custom controls, use this:
<ContentView
x:Class="demo2.simplecontrol.View1"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<ContentView.Content>
<StackLayout>
<Entry x:Name="label1" />
<Entry x:Name="label2" />
</StackLayout>
</ContentView.Content>
public partial class View1 : ContentView
{
public View1 ()
{
InitializeComponent ();
}
public static readonly BindableProperty Label1Property= BindableProperty.Create(
nameof(Label1),
typeof(string),
typeof(View1),
"",
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (newValue != null && bindable is View1 control)
{
var actualNewValue = (string)newValue;
control.label1.Text = actualNewValue;
}
});
public string Label1 { get; set; }
public static readonly BindableProperty Label2Property = BindableProperty.Create(
nameof(Label2),
typeof(string),
typeof(View1),
"",
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (newValue != null && bindable is View1 control)
{
var actualNewValue = (string)newValue;
control.label2.Text = actualNewValue;
}
});
public string Label2 { get; set; }
}
Then you can use this custom view in ContentPage.
<ContentPage
x:Class="demo2.simplecontrol.Page10"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:demo2.simplecontrol">
<ContentPage.Content>
<StackLayout>
<Label
HorizontalOptions="CenterAndExpand"
Text="Welcome to Xamarin.Forms!"
VerticalOptions="CenterAndExpand" />
<local:View1 Label1="{Binding text1}" Label2="{Binding text2}" />
</StackLayout>
</ContentPage.Content>
public partial class Page10 : ContentPage, INotifyPropertyChanged
{
private string _text1;
public string text1
{
get { return _text1; }
set
{
_text1 = value;
RaisePropertyChanged("text1");
}
}
private string _text2;
public string text2
{
get { return _text2; }
set
{
_text2 = value;
RaisePropertyChanged("text2");
}
}
public Page10 ()
{
InitializeComponent ();
text1 = "test1";
text2 = "test2";
this.BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Finally, you get data coming from web api for CategoryList, so you can add break point to check if having data.
'I'm not sure, but apparently the page is changing the binding context of your view.
Set a breakpoint inside your OnBindingContextChanged overridden method of your view and debug it. If it is confirmed, instantiate your view model from your page instead.
I'm developing an app with xamarin forms and the MVVM pattern. I have a page with a listview that has three buttons but all the time with only 2 visibles and change the visibility of two of them when I press a button. The problem is that for the first ten items it works like supposed to be, press the button and dissapear and appear the other, but after the 10th item when I press the button it dissapear but the other doesn't appear until I scrool the list view to a position where the item is out of the screen. When the item is out of the screen and come back to be on the screen, the button appear. The visibility of the buttons is controlled changing a boolean property that is binded to the IsVisible property of the button and one of them with a converter to negate the value of the property. This is a repository that you can clone and see the code and test, maybe is something with my Visual Studio.
Initially, I thought it could be for a race condition and made the method that change the variable synchronous but it doesn't work.
This is my list view
<ListView ItemsSource="{Binding Items}"
HasUnevenRows="True"
SeparatorVisibility="None"
IsRefreshing="False">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}"/>
<StackLayout Orientation="Horizontal">
<Button Text="One"
HorizontalOptions="CenterAndExpand"
TextColor="Green"
BackgroundColor="White"
BorderColor="Green"
BorderWidth="1"
WidthRequest="150" />
<Button Text="Two"
HorizontalOptions="CenterAndExpand"
BackgroundColor="Green"
TextColor="White"
Command="{Binding TestCommand}"
WidthRequest="150"
IsVisible="{Binding TestVariable, Converter={StaticResource negate}}" />
<Button Text="Three"
HorizontalOptions="CenterAndExpand"
BackgroundColor="Red"
Command="{Binding TestCommand}"
TextColor="White"
WidthRequest="150"
IsVisible="{Binding TestVariable}" />
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The viewmodel
public class ListViewTestModel : BaseViewModel
{
private List<ListItemTestModel> items;
public List<ListItemTestModel> Items
{
get => items;
set
{
SetValue(ref items, value);
}
}
public ListViewTestModel()
{
List<ListItemTestModel> itemList = new List<ListItemTestModel>();
for (int i = 0; i < 40; i++)
{
itemList.Add(new ListItemTestModel { Name = "Test" });
}
Items = itemList;
}
}
And another view model that is binded to each item in the listView
public class ListItemTestModel : BaseViewModel
{
private bool testVariable;
public string Name { get; set; }
public bool TestVariable
{
get
{
return testVariable;
}
set
{
SetValue(ref testVariable, value);
}
}
public Command TestCommand { get; set; }
public ListItemTestModel()
{
TestCommand = new Command(() =>
{
TestMethod();
});
}
public void TestMethod()
{
TestVariable = !TestVariable;
}
}
the BaseViewModel
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
{
return;
}
backingField = value;
OnPropertyChanged(propertyName);
}
}
And the codebehind of the page
public partial class MainPage : ContentPage
{
public ListViewTestModel ViewModel { get; }
public MainPage()
{
ViewModel = new ListViewTestModel();
BindingContext = ViewModel;
InitializeComponent();
}
}
I suggest listview Caching Strategy may case this issue, the default value is RetainElement for ListView, so using CachingStrategy="RecycleElement" in ListView.
About listview Caching Strategy, you can take a look:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/performance#caching-strategy
You should definitely go to ObservableCollection type for your items thus you'll be able to observe and display any changes
private ObservableCollection<ListItemTestModel> items;
public ObservableCollection<ListItemTestModel> Items
{
get => items;
set => SetValue(ref items, value);
}
And you should set your BindingContext AFTER the InitializeComponent() method or property changed will be propagate before your view is initialized.
public MainPage()
{
InitializeComponent();
BindingContext = new ListViewTestModel();;
}
public ListViewTestModel()
{
List<ListItemTestModel> itemList = new List<ListItemTestModel>();
for (int i = 0; i < 40; i++)
{
itemList.Add(new ListItemTestModel { Name = "Test" });
}
Items = new ObservableCollection<ListItemTestModel>(itemList);
}
I have created view AddonPickerControl that is a horizontal StackLayout with AddonControls. The problem is that Pages that includes AddonPickerListView loads about 2sec, its too long.
I have tried to achive same result with binding addons to a ListView, but the problem is that each cell have to have a counter that describes how much specific addon has been picked. I have no Idea how to do this in ViewCell, so I decided to StackLayout.
public partial class AddonPickerControl : ContentView
{
public AddonPickerControl (AddonPicker addonPicker)
{
InitializeComponent ();
_addonPicker = addonPicker;
BindingContext = _addonPicker;
}
private readonly AddonPicker _addonPicker;
protected override void OnAppearing()
{
foreach (var addon in _addonPicker.AvailableAddons)
{
var addonControl = new AddonControl(addon);
addonControl.AddonPicked += OnAddonPicked;
AddonContainer.Children.Add(addonControl);
}
}
...
}
public partial class AddonControl : ContentView
{
public AddonControl (Addon addon)
{
InitializeComponent ();
_addon = addon;
this.BindingContext = _addon;
}
private readonly Addon _addon;
...
}
How should I display an AddonPickerControl? Filling StackLayout with other views takes too much time. Or maybe it is possible to create a ViewCell that will have a counter that describes how much binded addon has been picked.
Here is an example of how you can have buttons in every item in a list view to update a count for that item.
First, here is a simple list view with a view cell with 3 labels and two buttons:
<ListView x:Name="listView" ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding ItemName}" />
<Label Text="Count:" />
<Label Text="{Binding Count}" />
<Button Text="+" Command="{Binding BtnClickPlusCommand}" />
<Button Text="-" Command="{Binding BtnClickMinusCommand}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Then in the code behind:
public partial class MainPage : ContentPage
{
public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();
public MainPage()
{
InitializeComponent();
for (int i=1; i<11; i++)
{
Item item = new Item { ItemName = $"Item {i}", Count = "5" };
Items.Add(item);
}
BindingContext = this;
}
}
And the Item class which will have your click handlers and is a simple view model as it implements INotifyPropertyChanged:
public class Item : INotifyPropertyChanged
{
public string ItemName { get; set; }
int _count;
public ICommand BtnClickPlusCommand { get; private set; }
public ICommand BtnClickMinusCommand { get; private set; }
public Item()
{
BtnClickPlusCommand = new Command(btnClickPlus);
BtnClickMinusCommand = new Command(btnClickMinus);
}
void btnClickPlus()
{
Count = (++_count).ToString();
}
void btnClickMinus()
{
Count = (--_count).ToString();
}
public string Count
{
get
{
return _count.ToString();
}
set
{
int j;
if (Int32.TryParse(value, out j))
{
_count = j;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
}
else
Console.WriteLine("value could not be parsed to int");
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So, in this case we have essentially created a view model for each item so we can have the command that will handle the button click in the actual Item object that is associated with the button, so we just have to update the count. And using bindings, the UI is updated automatically with the new count. The results:
The following XF app (code below) creates a simple ListView with 2 custom cells. Tapping on a cell uses the IsVisible property to show the second label.
On Android this works great as the size of the ViewCell resizes to fit the currently displayed contents. When the Detail item is made visible, the ViewCell expands to show the detail.
On iOS, this isn't working.
Here is how the app appears on first launch...
When you tap the first ViewCell, the IsVisible property is tripped and the Detail item shows. However, the ViewCell remains the same height causing it to overflow as seen below...
How can this be accomplished on the iOS side?
Here is the code...
XAML
<ContentPage.Content>
<ListView x:Name="___list" Margin="50" HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CellTap}" />
</StackLayout.GestureRecognizers>
<Label Text="{Binding Title}" />
<Label Text="{Binding Detail}" FontSize="30" IsVisible="{Binding ShowDetails}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
C#
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
___list.ItemsSource = new List<Element>() {
new Element() {
Title="First Element",
Detail = "First Element Details"
},
new Element() {
Title="Second Element",
Detail = "Second Element Details"
}
};
}
}
public class Element : INotifyPropertyChanged
{
public Element()
{
CellTap = new Command(() =>
{
ShowDetails = !ShowDetails;
});
}
public ICommand CellTap { get; private set; }
private string _title;
public string Title
{
get { return _title; }
set { if (_title != value) { _title = value; OnPropertyChanged("Title"); } }
}
private string _detail;
public string Detail
{
get { return _detail; }
set { if (_detail != value) { _detail = value; OnPropertyChanged("Detail"); } }
}
private bool _showDetails;
public bool ShowDetails
{
get { return _showDetails; }
set { if (_showDetails != value) { _showDetails = value; OnPropertyChanged("ShowDetails"); } }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewCell cannot automatically find out how high it is supposed to be. You'll have to support it by setting its Height or force it to update. Unfortunately, Heightis not bindable.
Option 1: use this if you have different heights per row and the list cannot figure out the correct height.
class CustomViewCell : ViewCell
{
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
// Do some calculation in here to get the height you need.
// Here we are using an example that bases the size on the result of ToString()
string text = BindingContext.ToString();
Height = 10 + ((int)(text[0]) - 65);
}
}
Option 2: change height dynamically (probably what you want)
void SomeEventHandler(object sender, EventArgs args)
{
// Let's assume an image was tapped...
var image = sender as Image;
// ...and the image is in a cell.
var viewCell = image.Parent.Parent as ViewCell;
// You would FIRST change the height of the content (in this case the image)
if (image.HeightRequest < 250)
{
image.HeightRequest = image.Height + 100;
// And THEN tell the cell to update (Note: you should not be required
// to subclass the cell)
viewCell.ForceUpdateSize();
}
}
Make sure that HasUnevenRows = true, otherwise forcing the update won't have an effect.
I have two different objects that are pointing at each other. The first object represents a division in a company. That object has two collection: Employees, which is all the employees working in the division and Project, which is all the special projects that are in progress within that division. So the first object looks like this:
public class Division : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
ObservableCollection<Employee> _employees;
ObservableCollection<Project> _projects;
public Division()
{
Employees = new ObservableCollection<Employee>();
Projects = new ObservableCollection<Project>();
}
public ObservableCollection<Employee> Employees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
}
}
}
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
if (_projects != value)
{
_projects = value;
PropertyChanged(this, new PropertyChangedEventArgs("Projects"));
}
}
}
public void AddNewProject()
{
this.Projects.Add(new Project(this));
}
}
Notice that when adding a new project to the division, I pass a reference to the division into that project, which looks like this:
public class Project : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
string _projectName;
DateTime _deadline = DateTime.Now;
Division _division;
ObservableCollection<Employee> _members;
public Project()
{
Members = new ObservableCollection<Employee>();
}
public Project(Division div)
{
Members = new ObservableCollection<Employee>();
Division = div;
}
public string ProjectName
{
get { return _projectName; }
set
{
if (_projectName != value)
{
_projectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ProjectName"));
}
}
}
public DateTime Deadline
{
get { return _deadline; }
set
{
if (_deadline != value)
{
_deadline = value;
PropertyChanged(this, new PropertyChangedEventArgs("Deadline"));
}
}
}
public Division Division
{
get { return _division; }
set
{
if (_division != value)
{
if (_division != null)
{
_division.Employees.CollectionChanged -= members_CollectionChanged;
}
_division = value;
if (_division != null)
{
_division.Employees.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Division"));
}
}
}
public ObservableCollection<Employee> Members
{
get { return _members; }
set
{
if (_members != value)
{
if (_members != null)
{
_members.CollectionChanged -= members_CollectionChanged;
}
_members = value;
if (_members != null)
{
_members.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Members"));
}
}
}
public ObservableCollection<Employee> AvailableEmployees
{
get
{
if (Division != null){
IEnumerable<Employee> availables =
from s in Division.Employees
where !Members.Contains(s)
select s;
return new ObservableCollection<Employee>(availables);
}
return new ObservableCollection<Employee>();
}
}
void members_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableEmployees"));
}
}
The reason I'm doing it like this is, that the project could have any type of team working on it, but only from within the division. So, when building a dashboard for the division, the manager could select any of the employees to that project but without putting in an employee that is already assigned to it. So, the AvailableEmployees property in the project object always keeps track of who is not already assigned to that project.
The problem I'm having is how to translate this into a UI. The experiment I've done so far looks like this:
<UserControl x:Class="Test.Views.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:local="clr-namespace:Test.Views"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<ListBox ItemsSource="{Binding Div.Projects}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent"
BorderThickness="0, 0, 0, 2"
BorderBrush="Black"
Margin="0, 0, 0, 5"
Padding="0, 0, 0, 5">
<StackPanel>
<TextBox Text="{Binding ProjectName}"/>
<ListBox ItemsSource="{Binding Members}">
<ListBox.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add Employee to Project"
Command="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AddEmployeeToProject}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add New Project"
Command="{Binding AddNewProject}" />
</StackPanel>
The view model associated with this view is as follows:
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private Division _div;
public TestViewModel(Division div)
{
Div = div;
AddNewProject = new DelegateCommand(OnAddNewProject);
AddEmployeeToProject = new DelegateCommand<Project>(OnAddEmployeeToProject);
}
public DelegateCommand AddNewProject { get; set; }
public DelegateCommand<Project> AddEmployeeToProject { get; set; }
public Division Div
{
get { return _div; }
set
{
if (_div != value)
{
_div = value;
PropertyChanged(this, new PropertyChangedEventArgs("Div"));
}
}
}
private void OnAddNewProject()
{
Div.AddNewProject();
}
private void OnAddEmployeeToProject(Project proj)
{
var availables = proj.AvailableEmployees;
if (availables.Count > 0)
{
proj.Members.Add(availables[0]);
}
}
}
However, I cannot get the combobox for each employee in each project to work. It seems like the selected item/value is bound to the itemssource, and each time the combobox turns out blank. I've tried to do this also with SelectedValue and SelectedItem properties for the combobox, but none worked.
How do I get these two separated. Is there anything else I'm missing here?
OK. After so many experiments the best solution I came up with was to create my own user control that is composed of both a button and a combobox that imitate the behavior I was expecting of the combobox on it own.
First, I had a really stupid mistake in the model where both lists of members Project and Division contain the same instances of Employee, which makes the AvailableEmployees property buggy. What I really needed to do is to create a list of copies of employees in the Project instead of just references.
In any case, I created a new user control and called it DynamicSourceComboBox. The XAML of this control looks like this:
<Grid>
<Button x:Name="selected"
Content="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=SelectedValue}"
Click="selected_Click"/>
<ComboBox x:Name="selections"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=ItemsSource}"
DisplayMemberPath="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=DisplayMemberPath}"
Visibility="Collapsed"
SelectionChanged="selections_SelectionChanged"
MouseLeave="selections_MouseLeave"/>
</Grid>
I have here a few bindings from the button and the combobox to properties in my user control. These are actually dependency properties. The code-behind of my user control looks like this:
public partial class DynamicSourceComboBox : UserControl
{
public DynamicSourceComboBox()
{
InitializeComponent();
}
public object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(DynamicSourceComboBox), new PropertyMetadata(null));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
ComboBox.ItemsSourceProperty.AddOwner(typeof(DynamicSourceComboBox));
public string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty =
ComboBox.DisplayMemberPathProperty.AddOwner(typeof(DynamicSourceComboBox));
private void selected_Click(object sender, RoutedEventArgs e)
{
selected.Visibility = Visibility.Hidden;
selections.Visibility = Visibility.Visible;
selections.IsDropDownOpen = true;
}
private void selections_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
if (e.AddedItems.Count == 1)
{
var item = e.AddedItems[0];
Type itemType = item.GetType();
var itemTypeProps = itemType.GetProperties();
var realValue = (from prop in itemTypeProps
where prop.Name == DisplayMemberPath
select prop.GetValue(selections.SelectedValue)).First();
SelectedValue = realValue;
}
}
private void selections_MouseLeave(object sender, MouseEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
}
}
These dependency properties imitate the properties with similar names in ComboBox but they are hooked up to the internal combobox and the button in a way that makes them behave together as a single complex combobox.
The Click event in the button hides it and present the combobox to make the effect of just a box that is opening. Then I have a SelectionChanged event in the combobox firing to update all the needed information and a MouseLeave event just in case the user doesn't make any real selection change.
When I need to use the new user control, I set it up like this:
<local:DynamicSourceComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorLevel=1, AncestorType=ListBox}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
SelectedValue="{Binding FirstName, Mode=TwoWay}"/>
Of course, for all of it to work, I have to make a lot of hookups with PropertyChanged events in the models, so the Projects instance will know to raise a PropertyChanged event for AvailableEmployees any time a change is made, but this is not really the concern of this user control itself.
This is a pretty clunky solution, with a lot of extra code that is a bit hard to follow, but it's really the best (actually only) solution I could have come up with to the problem I had.