Binding ContentView to ContentPage - c#

I am trying to implement a xamarin app that will have a MainPage like a container that will host the rest of my pages(as content view?).
MainPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:TalosLib"
mc:Ignorable="d"
x:Class="TalosLib.MainPage">
<ContentPage.Content>
<StackLayout >
<StackLayout.Resources>
<DataTemplate x:Key="login">
<views:LoginPage />
</DataTemplate>
</StackLayout.Resources>
<ContentView Content="{Binding CurrentView}" ControlTemplate="{StaticResource login}"/>
<!--<CollectionView ItemsSource="{Binding CurrentView}" ItemTemplate="{StaticResource login}"/>-->
</StackLayout>
</ContentPage.Content>
MainPageModel.cs
public class MainPageModel : FreshBasePageModel
{
//private ObservableCollection<LoginPageModel> _currentView;
//public ObservableCollection<LoginPageModel> CurrentView
//{
// get { return _currentView; }
// set { _currentView = value; RaisePropertyChanged("CurrentView"); }
//}
private LoginPageModel _currentView;
public LoginPageModel CurrentView
{
get { return _currentView; }
set { _currentView = value; RaisePropertyChanged("CurrentView"); }
}
public override void Init(object initData)
{
base.Init(initData);
//CurrentView = new ObservableCollection<LoginPageModel>();
//CurrentView.Add(new LoginPageModel());
CurrentView = new LoginPageModel();
RaisePropertyChanged(nameof(CurrentView));
}
}
Right now i am trying just to show the LoginPage but it doesn't appear. I managed to make it work if i used the commented parts of the code. i am using FreshMVVM. Any thoughts?

Control templates help you define the root view like navbar or headers in all pages. I am not sure why you want to bind content property if you want to use a static resource. If you are going to change the content then we can use data templates and use a converter to convert the ViewModel to view.
If you are interested to change the content of the ContentView, then you can use data templates as follows:
<ResourceDictionary>
<views:DataTemplateToViewConverter x:Key="dataTemplateToViewConverter" />
<DataTemplate x:Key="Login">
<views:LoginView />
</DataTemplate>
<DataTemplate x:Key="Demo">
<views:DemoView />
</DataTemplate>
</ResourceDictionary>
<ContentView x:Name="contentview" Content="{Binding MyTemplate, Converter={StaticResource dataTemplateToViewConverter}}" />
<Button
Command="{Binding Clicked1}"
Text="1" />
<Button
Command="{Binding Clicked2}"
Text="2" />
In your ViewModel, you can use the command interface and set the templates
on clicked commands.. don't forget to create your MyTemplate bindable property.
private void Clicked2Called(object obj)
{
MyTemplate = "DemoView";
}
private void Clicked1Called(object obj)
{
MyTemplate = "Login";
}
In your converter you can do as follows:
public class DataTemplateToViewConverter : IValueConverter
{
public DataTemplateToViewConverter()
{
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.ToString() == "Login")
return new LoginView();
else
return new DemoView();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
There are lots of ways to do this still better...I have used buttons to change the content, I am not sure how you wish to change the views when the menu items are selected. Hope it helps you solve your problem.

Related

How do I bind an ImageSource to a specific element's property in an ObservableCollection?

I would really like to know how I can bind an ImageSource to a specific element's property in an ObservableCollection... Right now I have this:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyNameSpace"
x:Class="MyNameSpace.MainPage"
x:Name = "Main"
Padding="5,20,5,5"
BindingContext="{x:Reference Name=Main }">
<Grid
<ImageButton
Grid.Column="0"
Grid.Row="0"
Source="{Binding _hand[0].ResourceId , Converter={StaticResource
StringToSourceConverter}}"
>
</ImageButton>
<ImageButton
Grid.Column="1"
Grid.Row="0"
Source="{Binding _hand[1].ResourceId , Converter={StaticResource
StringToSourceConverter}}"
>
</ImageButton>
<ImageButton
Grid.Column="0"
Grid.Row="1"
Source="{Binding _hand[2].ResourceId , Converter={StaticResource
StringToSourceConverter}}"
>
</ImageButton>
<ImageButton
Grid.Column="1"
Grid.Row="1"
Source="{Binding _hand[3].ResourceId , Converter={StaticResource
StringToSourceConverter}}"
>
</ImageButton>
</Grid>
</ContentPage>
I would like to bind the ImageSource to the following ObservableCollection of Cards...
public partial class MainPage : ContentPage, INotifyPropertyChanged
{
private ObservableCollection<Card> _hand;
public MainPage()
{
Init();
InitializeComponent();
}
private void Init()
{
_hand = new ObservableCollection<Card>()
{
new Card("image1.jpg"),
new Card("image2.jpg"),
new Card("image3.jpg"),
new Card("image4.jpg")
};
}
}
My Card class looks something like this:
public Card ( string resourceId)
{
ResourceId = resourceId;
}
public string ResourceId { get; set; }
The Converter used :
public class ToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string ResourceId = value.ToString();
if (String.IsNullOrWhiteSpace(ResourceId))
return null;
return ImageSource.FromResource(ResourceId);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
My question now is how do I make this code work? Also I have methods that switch elements in the collection. Where do I implement PropertyChangedEvent? Thank you guys a lot :)

Getting a property value from a ContentView (child view) to the ContentPage (parent)

As a not so experienced Xamarin developer I am trying to retrieve a property value from a child page (ContentView) in my Parent page (ContentPage).
I can find quite some examples how to get / set the value from the parent page to the child page but not the other way around.
Some more details:
In my ContentPage I have a CarouselView, this CarouselView has a DataTemplate which contains a ContentView, this ContentView has also a CarouselView inside it with 2 layers / 2 vertical carousel items.
When the position of the CarouselView inside the ContentView (the child page), changes position to the second item, the IndicatorView in the parent page should be set to invisible.
I am not so experienced using a BindableProperty but I think that is the way to go. I got it setup as following for now:
The parent page / ContentPage:
<local:ExtendedCarouselView
x:Name="carousel"
HorizontalScrollBarVisibility="Never"
IndicatorView="activityIndicatorView"
IsScrollAnimated="False"
ItemsSource="{Binding ActivityData}"
Position="{Binding Position, Mode=TwoWay}"
VerticalOptions="FillAndExpand">
<local:ExtendedCarouselView.ItemTemplate>
<DataTemplate>
<Frame Style="{StaticResource CarouselWorkaround}">
<local:PCSActivityOverviewTemplate x:Name="testy" />
</Frame>
</DataTemplate>
</local:ExtendedCarouselView.ItemTemplate>
</local:ExtendedCarouselView>
<IndicatorView
x:Name="activityIndicatorView"
Padding="0,0,0,30"
IndicatorColor="{DynamicResource TranslucidBlack}"
IsVisible="{Binding InnerCarouselViewPosition, Converter={StaticResource IndicatorVisibilityConverter}, Mode=TwoWay}"
SelectedIndicatorColor="{DynamicResource BaseTextColor}"
VerticalOptions="Start" />
The child page / ContenView (XAML):
<ContentView.Content>
<CarouselView
x:Name="carousel"
ItemsSource="{Binding ., Converter={StaticResource OneToManyConverter}, ConverterParameter=2}"
VerticalOptions="FillAndExpand"
VerticalScrollBarVisibility="Never"
PositionChanged="carousel_PositionChanged"> <!-- The event which should change the property 'InnerCarouselViewPosition' -->
<CarouselView.ItemTemplate>
<grial:IntMemberTemplateSelector MemberName="Position">
<grial:IntMemberTemplateSelector.Items>
<!-- CAROUSEL'S PAGE 0 -->
<grial:IntMemberTemplateSelectorItem Value="0">
<DataTemplate>
<!-- Other elements... -->
</DataTemplate>
</grial:IntMemberTemplateSelectorItem>
<!-- CAROUSEL'S PAGE 1 -->
<grial:IntMemberTemplateSelectorItem Value="1">
<DataTemplate>
<!-- Other elements... -->
</DataTemplate>
</grial:IntMemberTemplateSelectorItem>
</grial:IntMemberTemplateSelector.Items>
</grial:IntMemberTemplateSelector>
</CarouselView.ItemTemplate>
</CarouselView>
</ContentView.Content>
The ContenView (C#/.cs):
public partial class PCSActivityOverviewTemplate : ContentView
{
public static BindableProperty CurrentChildCarouselViewLocationProperty =
BindableProperty.Create(
nameof(CurrentChildCarouselViewLocationProperty),
typeof(int),
typeof(CarouselView),
defaultValue: 1);
public int CurrentChildCarouselViewLocation
{
get { return (int)GetValue(CurrentChildCarouselViewLocationProperty); }
set { SetValue(CurrentChildCarouselViewLocationProperty, value); }
}
private void carousel_PositionChanged(object sender, PositionChangedEventArgs e)
{
CarouselView _carouselView = (CarouselView)sender;
CurrentChildCarouselViewLocationProperty = _carouselView.Position;
}
... code omitted
}
When the inner carousel view position's changes then the bindable property should be set, this property should be used in the parent page to set the indicator view visible / invisible using a converter (position 0 = visible, position 1 = invisible).
For some, probably a quite obvious, reason the above is not working.
The Visibility converter:
public class CarouselIndicatorVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value != 1;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? 0 : 1;
}
}
UPDATE*
I also tried to use a binding as the Position Property in my ViewModel, while the binding value changes, I cant access it in the Parent page nothing happens, the converter is not triggered), I removed the bindable property.
The new Content page XAML (the parent Carousel):
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
...
>
<ContentPage.Resources>
<ResourceDictionary>
<local:CarouselIndicatorVisibilityConverter x:Key="IndicatorVisibilityConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<Grid>
<Grid>
<local:ExtendedCarouselView
x:Name="carousel"
HorizontalScrollBarVisibility="Never"
IndicatorView="activityIndicatorView"
IsScrollAnimated="False"
ItemsSource="{Binding ActivityData}"
Position="{Binding Position, Mode=TwoWay}"
VerticalOptions="FillAndExpand">
<local:ExtendedCarouselView.ItemTemplate>
<DataTemplate>
<Frame Style="{StaticResource CarouselWorkaround}">
<local:PCSActivityOverviewTemplate x:Name="testy" />
</Frame>
</DataTemplate>
</local:ExtendedCarouselView.ItemTemplate>
</local:ExtendedCarouselView>
<IndicatorView
x:Name="activityIndicatorView"
Padding="0,0,0,30"
IndicatorColor="{DynamicResource TranslucidBlack}"
IsVisible="{Binding BindingContext.CurrentChildCarouselViewLocation, Source={x:Reference carousel}, Converter={StaticResource IndicatorVisibilityConverter}, Mode=TwoWay}"
SelectedIndicatorColor="{DynamicResource BaseTextColor}"
VerticalOptions="Start" />
</Grid>
</Grid>
</ContentPage.Content>
</ContentPage>
XAML of the child page:
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
...
>
<ContentView.Content>
<CarouselView
x:Name="carousel"
IsBounceEnabled="False"
ItemsSource="{Binding ., Converter={StaticResource OneToManyConverter}, ConverterParameter=2}"
Position="{Binding CurrentCarouselViewLocation}"
PositionChanged="carousel_PositionChanged"
VerticalOptions="FillAndExpand"
VerticalScrollBarVisibility="Never">
<CarouselView.ItemsLayout>
<LinearItemsLayout
ItemSpacing="0"
Orientation="Vertical"
SnapPointsAlignment="Start"
SnapPointsType="MandatorySingle" />
</CarouselView.ItemsLayout>
<CarouselView.ItemTemplate>
<grial:IntMemberTemplateSelector MemberName="Position">
<grial:IntMemberTemplateSelector.Items>
<!-- CAROUSEL'S PAGE 0 -->
<grial:IntMemberTemplateSelectorItem Value="0">
<DataTemplate>
.. more elements omitted
</DataTemplate>
</grial:IntMemberTemplateSelectorItem>
<!-- CAROUSEL'S PAGE 1 -->
<grial:IntMemberTemplateSelectorItem Value="1">
<DataTemplate>
.. more elements omitted
</DataTemplate>
</grial:IntMemberTemplateSelectorItem>
</grial:IntMemberTemplateSelector.Items>
</grial:IntMemberTemplateSelector>
</CarouselView.ItemTemplate>
</CarouselView>
</ContentView.Content>
</ContentView>
The ViewModel
namespace PCS2.APP.ViewModels
{
public class ActivityOverviewViewModel : ObservableObject
{
private List<ActivityLocation> activityData;
private readonly IRoutingService _routingService;
private double _screenOpacity;
private bool _showLoadingAnimation;
private int? _clientId;
private int _position;
private int _innerCarouselPosition;
// Position of the Parent page CarouselView
public int Position
{
get { return _position; }
set { SetProperty(ref _position, value); }
}
// Data source for the child data
public List<ActivityLocation> ActivityData
{
get { return activityData; }
set { SetProperty(ref activityData, value); }
}
public double ScreenOpacity
{
get { return _screenOpacity; }
set { SetProperty(ref _screenOpacity, value); }
}
public bool ShowLoadingAnimation
{
get { return _showLoadingAnimation; }
set { SetProperty(ref _showLoadingAnimation, value); }
}
public ActivityOverviewViewModel(int? clientId = null, IRoutingService routingService = null)
: base(listenCultureChanges: true)
{
_clientId = clientId;
_routingService = routingService ?? Locator.Current.GetService<IRoutingService>();
LoadData();
}
private async void LoadData()
{
try
{
ShowLoadingAnimation = true;
ScreenOpacity = 0.1;
// Getting the data
var _activitiesData = await App.Database.GetActivityDataAsync(_clientId, DateTime.UtcNow);
ActivityData = _activitiesData;
}
catch (Exception ex)
{
throw;
}
finally
{
ShowLoadingAnimation = false;
ScreenOpacity = 1.0;
}
}
}
}

How to Bind Local Embedded Image in ListView in Xamarin Forms?

I have a ListView in XAML and a List<string> that holds local embedded image paths. I am not able to show images in List. By the way I am able to show as a single image by
<Image Source="{local:ImageResource TypingApplication.Images.Icons.Search.png}" />
But I cannot show the images in ListView. Here is my XAML code
<ListView x:Name="ListView"
ItemsSource="{Binding ListItems}"
IsEnabled="True"
IsVisible="True"
RowHeight="40"
Opacity="0.9">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Image Source="{local:ImageResource TypingApplication.Images.Icons.{Binding .}}"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I have added ImageResourceExtension in Extensions folder and xmlns:local="clr-namespace:TypingApplication.Extensions" in XAML, as I mentioned I can show Single Image, only there is problem with ListView.
Here is my C# code that contains List and Constructor
public List<string> ListItems
{
get
{
return new List<string>()
{
"Home.png",
"Favorite.png",
"Search.png"
};
}
}
public HomePage()
{
InitializeComponent();
this.BindingContext = this;
}
Please note that I am using shared Images in my project. I have set Properties of all Images to Embedded resource in SolutionExplorer.
Change list to ObservableCollection
IValueConverter implementation to convert your binding to desired value
The image property should be set to EmbeddedResource
public class EmbeddedToImageSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string fileName && parameter is String assemblyName)
{
try
{
var imageSource = ImageSource.FromResource(assemblyName + "." + fileName, typeof(EmbeddedToImageSourceConverter).GetTypeInfo().Assembly);
return imageSource;
}
catch (Exception)
{
return value;
}
}
else
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
XAML
<ContentPage.Resources>
<local:EmbeddedToImageSourceConverter x:Key="converter" />
</ContentPage.Resources>
In the listview add binding w.r.to converter resource we just created.
<Image Source="{Binding ., Converter={StaticResource converter}, ConverterParameter='TypingApplication.Images.Icons'}" />
If you are not using View Model (MVVM), you can directly specify image file's name in XAML as:
<Image Source="{Binding Source='ImageFileName.png', Converter={StaticResource converter}, ConverterParameter='TypingApplication.Images.Icons'}" />
If you want to add Embedded image in listview, according to json's reply, your binding have some problem, you can use IValueConverter to convert image path as correct.
I do one sample according to your code, you can take a look:
<ListView HasUnevenRows="True" ItemsSource="{Binding ListItems}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Image
HeightRequest="100"
Source="{Binding ., Converter={StaticResource imageconverter}}"
WidthRequest="100" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentPage.Resources>
<local:imageconverter x:Key="imageconverter" />
</ContentPage.Resources>
The Image converter:
public class imageconverter : IValueConverter
{
public string Source { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Source = (string)value;
if (Source == null)
return null;
// Do your translation lookup here, using whatever method you require
var imageSource = ImageSource.FromResource("demo3."+Source, typeof(ImageResourceExtension).GetTypeInfo().Assembly);
return imageSource;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You can change demo3 as TypingApplication according to your code.
public partial class Page14 : ContentPage
{
public ObservableCollection<string> ListItems { get; set; }
public Page14()
{
InitializeComponent();
ListItems = new ObservableCollection<string>()
{
"image1.jpg","image2.png","image3.png"
};
this.BindingContext = this;
}
}
As Prateek's reply, I suggest you can change List<> to Observablecollection<>, because it implement INotifyPropertyChanged interface, notify data changed.
https://learn.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1?view=netframework-4.8

Change the Background Color of an ListView Item on Xamarin Forms

I have a ListView that binds its items from an ObservableCollection, and a Button that changes an "Amount" property of a specific object of that ObservableCollection. And I want to change the BackgroundColor of these Items whose "Amount" has already been changed.
I've searched for a solution for that, but I couldn't find any.
Does anybody know a way for solving that?
One way to do it would be to add a new property, something like HasAmountChanged, bind the background color of the viewcell to that property, and use a ValueConverter to set the color. This would look something like the following:
The object class with the properties:
public class MyObject : INotifyPropertyChanged
{
double amount;
bool hasAmountChanged = false;
public event PropertyChangedEventHandler PropertyChanged;
public MyObject(double amount)
{
this.amount = amount;
}
public double Amount
{
get => amount;
set
{
if (amount != value)
{
amount = value;
OnPropertyChanged(nameof(Amount));
HasAmountChanged = true;
}
}
}
public bool HasAmountChanged
{
get => hasAmountChanged;
set
{
if (hasAmountChanged != value)
{
hasAmountChanged = value;
OnPropertyChanged(nameof(HasAmountChanged));
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The view. Notice the stacklayout inside the ViewCell, that's where the background color is set:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Delete"
x:Class="Delete.MainPage">
<ContentPage.Resources>
<ResourceDictionary>
<local:ListViewBackgroundColorConverter x:Key="ListViewColorConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout>
<Button Text="Click Me" Clicked="ButtonClicked" />
<ListView ItemsSource="{Binding MyItemsSource}" HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Spacing="15"
BackgroundColor="{Binding HasAmountChanged, Converter={StaticResource ListViewColorConverter}}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<Label Text="FOO 1"/>
<Label Text="{Binding Amount}"/>
<Label Text="{Binding HasAmountChanged}" />
<Label Text="FOO 4"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
The code behind of the view, included for completeness:
public partial class MainPage : ContentPage
{
public ObservableCollection<MyObject> MyItemsSource { get; set; }
public MainPage()
{
InitializeComponent();
MyItemsSource = new ObservableCollection<MyObject>
{
new MyObject(1.14),
new MyObject(1.14),
new MyObject(1.14),
new MyObject(1.14),
new MyObject(1.14),
};
BindingContext = this;
}
void ButtonClicked(object sender, EventArgs e)
{
var rnd = new Random();
var myObject = MyItemsSource[rnd.Next(0, MyItemsSource.Count)];
myObject.Amount = 5.09;
}
}
And finally the most important part, the converter:
public class ListViewBackgroundColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Color.LawnGreen : Color.DarkRed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Note that you would actually want to check it's a bool coming in and handle that as well.
You could implement an array of booleans and change them to true when Amount gets changed. Then you might want to create a custom renderer for the color of each ListView.

Conducting MahApps.Metro HamburgerMenu with Caliburn Micro

I am having a few issues with using Caliburn Micro's Conductor<>.Collection.OneActive with MahApps.Metro HamburgerMenu. From a few samples, but none of them address my scenario.
All of my code is available in this Github repository.
I want to show a set of panes inside a HamburgerMenu. Each pane has a title and a display name:
public interface IPane : IHaveDisplayName, IActivate, IDeactivate
{
PackIconModernKind Icon { get; }
}
In my case, IPane is implemented using PaneViewModel:
public class PaneViewModel : Screen, IPane
{
public PaneViewModel(string displayName, PackIconModernKind icon)
{
this.Icon = icon;
this.DisplayName = displayName;
}
public PackIconModernKind Icon { get; }
}
This has the following view:
<UserControl x:Class="CaliburnMetroHamburgerMenu.Views.PaneView"
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"
d:DesignHeight="300" d:DesignWidth="300"
Padding="12"
Background="Pink">
<StackPanel Orientation="Vertical">
<TextBlock Text="Non-bound text" />
<TextBlock x:Name="DisplayName" FontWeight="Bold" />
</StackPanel>
</UserControl>
My shell view model is also quite simple. It inherits from Conductor<IPane>.Collection.OneActive, and takes in a list of panes that it adds to its Items collection:
public class ShellViewModel : Conductor<IPane>.Collection.OneActive
{
public ShellViewModel(IEnumerable<IPane> pages)
{
this.DisplayName = "Shell!";
this.Items.AddRange(pages);
}
}
Now, this is very it gets fuzzy for me. This is an excerpt from ShellView.xaml:
<controls:HamburgerMenu
ItemsSource="{Binding Items, Converter={StaticResource PaneListToHamburgerMenuItemCollection}}"
SelectedItem="{Binding ActiveItem, Mode=TwoWay, Converter={StaticResource HamburgerMenuItemToPane}}">
<ContentControl cal:View.Model="{Binding ActiveItem}" />
<controls:HamburgerMenu.ItemTemplate>
<DataTemplate>
<Grid x:Name="RootGrid"
Height="48"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<iconPacks:PackIconModern
Grid.Column="0"
Kind="{Binding Icon}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontSize="16"
Foreground="White"
Text="{Binding Label}" />
</Grid>
</DataTemplate>
</controls:HamburgerMenu.ItemTemplate>
</controls:HamburgerMenu>
To make this work, I rely on two converters (who quite frankly do more than they should have to). One converter takes a ICollection<IPane> and creates a HamburgerMenuItemCollection with HamburgerMenuIconItems that are now contain a two-way link using the Tag properties of both the view model and the menu item.
class PaneListToHamburgerMenuItemCollection : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var viewModels = value as ICollection<IPane>;
var collection = new HamburgerMenuItemCollection();
foreach (var vm in viewModels)
{
var item = new HamburgerMenuIconItem();
item.Label = vm.DisplayName;
item.Icon = vm.Icon;
item.Tag = vm;
vm.Tag = item;
collection.Add(item);
}
return collection;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The second converter converts between the view model and the menu item using this Tag whenever the SelectedItem changes:
class HamburgerMenuItemToPane : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((IPane)value)?.Tag;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((HamburgerMenuIconItem)value)?.Tag;
}
}
When I run this code, and click the items in the hamburger menu, the page switches every time. The issue is that when the app first runs, there is no selected pane, and you cannot set one using any of the activation overrides available in ShellViewModel (such as OnViewAttached or OnActivate, or event the constructor), as the converter code that hooks up the Tag hasn't run yet.
My requirements for a working solution:
Caliburn's conductor must be in charge, as there are views and view models further down the stack that depend on the activation logic to run.
It should be possible to activate the first item from Caliburn at some point during the activation of ShellViewModel
Should respect separation of concerns, i.e. the view model should not know that a hamburger menu is being used in the view.
Please see the GitHub repository for a solution that should run straight away.
I believe the issue is caused by the HamburgerMenu_Loaded method inside the control. If there is a selected item before the control loads, the content of the hamburger menu is replaced:
private void HamburgerMenu_Loaded(object sender, RoutedEventArgs e)
{
var selectedItem = this._buttonsListView?.SelectedItem ?? this._optionsListView?.SelectedItem;
if (selectedItem != null)
{
this.SetCurrentValue(ContentProperty, selectedItem);
}
}
In your case, the ContentControl is removed and your Conductor cannot do its job.
I'm trying to see if this behavior can be changed in MahApps directly, by changing the code to something like this:
if (this.Content != null)
{
var selectedItem = this._buttonsListView?.SelectedItem ?? this._optionsListView?.SelectedItem;
if (selectedItem != null)
{
this.SetCurrentValue(ContentProperty, selectedItem);
}
}

Categories