I'm trying to add a common control I want to appear at the bottom of every content page in the app (these will all be inside a TabBar). I made a ControlTemplate in my App.xaml, and the Picker I placed in the bottom appears, but the ItemsSource property binding isn't working (there are no items visible).
I'm not sure how to get this to work. I'm new to Xamarin/MAUI, and am open to suggestions for different approaches if I'm going in the wrong direction to accomplish this.
I've tried using TemplateBinding instead of Binding in the XAML, and I've also placed the same properties in the App.xaml.cs and the AppShell.xaml.cs code-behind files, in case the bindings were being redirected there, which didn't make a difference. I also started out with the Environments property just being of type Env[], and switched to ObservableCollection as a troubleshooting measure (even though the collection is obviously static).
App.xaml
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:My.App"
x:Class="My.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
<ResourceDictionary>
<ControlTemplate x:Key="InputPageTemplate">
<VerticalStackLayout BindingContext="{Binding Source={RelativeSource TemplatedParent}}">
<ContentPresenter />
<!-- ******************** BINDINGS NOT WORKING ******************** -->
<Picker ItemsSource="{Binding Environments}"
SelectedItem="{Binding AppConfig.Environment}" />
</VerticalStackLayout>
</ControlTemplate>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
MyPage.cs
public class MyPage : ContentPage
{
public MyPage()
{
if (!Application.Current!.Resources.TryGetValue("InputPageTemplate", out var resource) ||
resource is not ControlTemplate template)
{
throw new Exception("Missing InputPageTemplate control template");
}
var appConfig = new AppConfig { Environment = Env.B };
ViewModel = new MyViewModel(appConfig);
BindingContext = ViewModel;
ControlTemplate = template;
}
}
MyViewModel.cs
public class MyViewModel
{
public MyViewModel(AppConfig appConfig)
{
AppConfig = appConfig;
}
public AppConfig AppConfig { get; }
public ObservableCollection<Env> Environments => new(Enum.GetValues<Env>());
}
AppConfig.cs
public class AppConfig : INotifyPropertyChanged
{
private Env _environment;
public Env Environment
{
get => _environment;
set
{
_environment = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Env.cs
public enum Env
{
A,
B
}
The way to get the template to share a binding context with the content page is to include a path in the binding, like so:
<VerticalStackLayout
BindingContext="{Binding Source={RelativeSource TemplatedParent}, Path=BindingContext}">
<!-- Add this: ^^^^^^^^^^^^^^^^^^^^^ -->
Ok this is going to be long.
When you want to bind something in your ControlTemplate, you do not bind it directly.
You have to use different source.
There are few ways to do this. One is to use the word TemplateBinding.
The other is to set the BindingContext to Templated parent.
For example:
<Control Template...
<Grid BindingContext="{Binding Source={RelativeSource TemplatedParent}"...
<Button Command="{Binding MyCommand}"...
When you press that button, the binding will execute MyCommand, of "whatever" the control is during runtime.
So, If in your ViewModel you have GetDataCommand, you will do this in your View:
<mycontrols:MyView MyCommand="{Binding GetDataCommand}...
The deal here is to have this custom control class MyView with BindableProperty. That will allow you to bind between the ViewModel and the ControlTemplate.
public class MyView : ContentView{
public static readonly MyCommandProperty = ...
public ICommand MyCommand...
}
When I am not sure how something is done, I usually click F12 and browse the platform code. First, you get the idea how everything works, second you learn how to do it yourself.
Also, I recommend that you use the Dependency Injection, and limit the use of constructors. I also recommend CommunityToolkit.MVVM. It will save you time from implementing INotifyPropertyChanged. (And the possible mistakes from doing it wrong).
Related
Here is part of my FooPage.xaml
<Page.Resources>
<!-- I want this is regionToTextConverter from my FooPage.cs, how to do it? -->
<wpfCore:StringArrayToTextConverter
x:Key="regionConverter" />
</Page.Resources>
And somewhere also in this FooPage.xaml
<ListViewItem>
<DockPanel>
<Label Content="Something" />
<!-- I want to use regionToTextConverter from Page class here -->
<TextBox
Text="{Binding someValue, Mode=OneWay,
Converter={StaticResource regionConverter}}" />
</DockPanel>
</ListViewItem>
Now part of my FooPage.cs:
public StringArrayToTextConverter regionToTextConverter { get; private set; }
//Somewhere else
// additionalValueMapping is a dictionary
regionToTextConverter = new StringArrayToTextConverter()
{
additionalValueMapping = regionMapping
};
My question is, how do I use the regionToTextConverter from page instance in the xaml view? StaticResource may not be an option here because I need to some logic specifically in this case, I want to know how to correctly use it in this scenario.
To create a converter in the code-behind of your FooPage and use it in XAML, just add it to the Resources of the page as you would do in XAML. However, you can only add it before the call to InitializeComponent, otherwise the resource is not available via the StaticResource extension. Using DynamicResource here is not applicable, because Converter is not a dependency property.
public partial class FooPage : Page
{
public FooPage()
{
Resources.Add("regionConverter", new StringArrayToTextConverter()
{
additionalValueMapping = regionMapping
});
InitializeComponent();
// ...other code.
}
// ...other code.
}
In short, I want to make a collection of icons in a GridView from which the user can select one to change their profile icon.
I'm thinking that I can achieve that by declaring the icons I need on a ResourceDictionary
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:iconPacks="using:MahApps.Metro.IconPacks">
<iconPacks:PackIconMaterial x:Key="IconCarrot" Kind="Carrot"/>
<FontIcon x:Key="IconRobot" FontFamily="Segoe MDL2 Assets" Glyph=""/>
<!--As can we see, the icons are of different types-->
</ResourceDictionary>
Add that in App.xaml
<Application.Resources ...>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ResourceDictionaries/MyIcons.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Also, I created a class to instantiate a IconObject with two properties: the actual Icon and a string that I want to use like a tooltip text
public class IconObject
{
// I really don't know what type I need to use here
public object DisplayedIcon { get; set; }
// Tooltip
public string DisplayedToolTip { get; set; }
// Contructor
public IconObject(object theIcon, string theTooltip)
{
DisplayedIcon = theIcon;
DisplayedToolTip = theTooltip;
}
}
So, in code I have a ObservableCollection<IconObject> that I want to populate
private void PopulateTheObservableCollection()
{
IconObjects.Add(new IconObject(Application.Current.Resources["IconCarrot"], "Carrot"));
}
Finally, I'm trying to bind on a GridView
<GridView ItemsSource="{x:Bind IconObjects}">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid MaximumRowsOrColumns="6" Orientation="Horizontal" ItemHeight="50" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate x:DataType="local:IconObject" >
<Viewbox Child="{x:Bind DisplayedIcon}" ToolTipService.ToolTip="{x:Bind DisplayedToolTip}"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
But throws an error (and I can see why but I don't know ho to fix it).
The questions here are
Is this a good approach to doing this (to have a grid view populated with icons that the user can select and then use the icon selected)?
What type of property do I need to use to store an icon?
Do you have any suggestion?
(Thank you for your time and for reading all this)
What type of property do I need to use to store an icon?
Based on your code, you want to add the icon into Viewbox, the type of Child proeprty is UIElement, so you can set your DisplayedIcon property as UIElement type or keep object type and use Converter method to convert it. In addition, if you want to add the icon into Viewbox, you need to remove it in the ResourceDictionary prior and then add it into Viewbox, an instance of element cannot appear multiple times in XAML tree. For example:
Model:
public class IconObject
{
// I really don't know what type I need to use here
public UIElement DisplayedIcon { get; set; }
// Tooltip
public string DisplayedToolTip { get; set; }
// Contructor
public IconObject(UIElement theIcon, string theTooltip)
{
DisplayedIcon = theIcon;
DisplayedToolTip = theTooltip;
}
}
.xaml.cs:
Update:
private void PopulateTheObservableCollection()
{
ResourceDictionary dic = Application.Current.Resources.MergedDictionaries[0];
UIElement iconElement = dic["IconCarrot"] as UIElement;
dic.Remove("IconCarrot");
IconObjects.Add(new IconObject(iconElement, "Carrot"));
}
Is this a good approach to doing this
You can try to use DataTemplateSelector method to create different DataTemplate and Model based on your different types of icon. For more details about DataTemplateSelector, you can refer to this document.
I'm very new to WPF but quite experienced with .NET and C#. I am trying to create (what I though would be) a fairly simple CRUD admin desktop application for a website I plan on building.
WPF seems to be way more complicated than I expected it to be and after lots of Googling I've basically realised that everyone uses the MVVM pattern - fine. Now, with my existing .NET experience, I know I definitely want to to be using dependency injection. I've discovered that everything seems to be done within the ViewModel in WPF, including all the services and everything - fine again.
Now, onto my problem. I have set up a basic tab control and I'm binding the tab values to an enum using Enum.GetValues(). I want the view to change when I select a tab and the view will depend on which tab is selected. The problem is, I can't seem to get the view to show - it just shows a blank screen. The view is a custom UserControl I've created and defined as a resource and contains a grid and a bunch of buttons and stuff. I've omitted this from below as it doesn't seem relevant.
My MainWindow.xaml is pretty simple and looks like this:
<Window x:Class="Stc.Admin.MainWindow"
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"
xmlns:viewmodels="clr-namespace:Stc.Admin.ViewModels"
xmlns:views="clr-namespace:Stc.Admin.Views"
xmlns:local="clr-namespace:Stc.Admin"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding CurrentTab}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:GamesViewModel}">
<views:Games />
</DataTemplate>
</TabControl.Resources>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding DataContext.CurrentViewModel, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TabControl}}}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Here's my MainViewModel.cs:
public class MainViewModel
{
private readonly IViewModelFactory<GamesViewModel> _gamesViewModelFactory;
private ViewType _currentTab;
public ViewType CurrentTab
{
get
{
return _currentTab;
}
set
{
_currentTab = value;
ChangeView(_currentTab);
}
}
public ObservableCollection<ViewType> Tabs { get; }
public ViewModelBase CurrentViewModel { get; set; }
public MainViewModel(IViewModelFactory<GamesViewModel> gamesViewModelFactory)
{
_gamesViewModelFactory = gamesViewModelFactory;
Tabs = new ObservableCollection<ViewType>(Enum.GetValues(typeof(ViewType)).Cast<ViewType>().ToArray());
}
private void ChangeView(ViewType viewType)
{
switch (viewType)
{
case ViewType.Games:
CurrentViewModel = _gamesViewModelFactory.CreateViewModel();
break;
case ViewType.Listings:
break;
case ViewType.Users:
break;
case ViewType.Languages:
break;
case ViewType.Currencies:
break;
default:
break;
}
}
}
public enum ViewType
{
Games,
Listings,
Users,
Languages,
Currencies
}
GamesViewModel has service dependencies so it needs to be created using the factory.
And my DI setup in App.xaml.cs:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
IServiceProvider serviceProvider = this.createServiceProvider();
Window window = new MainWindow();
window.DataContext = serviceProvider.GetRequiredService<MainViewModel>();
window.Show();
base.OnStartup(e);
}
private IServiceProvider createServiceProvider()
{
IServiceCollection services = new ServiceCollection();
services.AddDbContext<StcContext>(options =>
options.UseSqlServer(#"Server=(localdb)\mssqllocaldb;Database=Stc;Integrated Security=True"));
services.AddSingleton<ICrudService<Game>, CrudService<Game>>();
services.AddSingleton<IViewModelFactory<GamesViewModel>, GamesViewModelFactory>();
services.AddScoped<MainViewModel>();
return services.BuildServiceProvider();
}
}
I have sorted this issue now. Being new to WPF, I didn't realise that I have to use INotifyPropertyChanged to get the UI to update after changing a property value on my ViewModel. I'd seen this used in a lot of the articles and tutorials I was seeing but didn't really understand what it was or how to apply it to my application.
The change I made was to implement this interface on my base ViewModel like so:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I then change my MainViewModel to inherit from the base class and changed the setter of the CurrentTab property to call OnPropertyChanged (with the name of the property) after I've changed the view/viewmodel property:
private ViewType _currentTab;
public ViewType CurrentTab
{
get
{
return _currentTab;
}
set
{
_currentTab = value;
ChangeView(_currentTab);
OnPropertyChanged(nameof(CurrentViewModel));
}
}
I believe this is telling the UI that something has changed and it needs to redraw itself. Correct me if I'm wrong or if that's an oversimplification.
I have what I'm sure is a ridiculously ignorant question, but I'm asking it anyways because I've searched and searched and either don't understand the solutions I'm seeing or not finding exactly the answer I seek.
I have an MVVM application. My XAML is setup with the DataContext set to the VM where the data items on the screen are populated from the VM's properties. My CodeBehind doesn't fiddle with the data, only things relating to the screen.
What I want to do now is bind certain UI elements to properties in the foo.xaml.cs (CodeBehind) file. For example, I want to specify FontSize's bound to properties in the CB so that in the WindowInitialized handler in the CB, it can detect screen sizes and change one variable to which all the screen items' FontSize= are bound.
I can solve this the wrong way by creating a public property in my VM and then "inject" the value from the CB into the VM. I know that will work, but it's a roundabout way to get the behavior I want, it's not at all straightforward, and I feel confident it's the wrong way to proceed.
I searched around and have tried things like:
FontSize="{Binding RelativeSource={RelativeSource Self},Path="MyFontSize"
(where "MyFontSize" is a public int property) and a variety of other examples I found, but none have worked.
So specifically, if my CodeBehind class is called NameChangeSetupMainWindow and that's where the "MyFontSize" property lives,
public partial class NameChangeSetupMainWindow : Window
{
private int m_fontSize = 14;
public int MyFontSize
{
get { return m_fontSize; }
set
{
if (m_fontSize != value))
{
m_fontSize = (value > 0) ? value : 10;
}
}
}
...
... rest of the class...
...
}
and the VM is called NameChangeSetupViewModel and that's where the "real" data lives and the DataContext points ala:
<Window.DataContext>
<local:NameChangeSetupViewModel/>
</Window.DataContext>
what is the syntax in XAML to bind just those UI items (tooltips related to the UI, font sizes, etc) to variables in the CodeBehind instead of housing them in the VM?
Thanks in advance for any guidance you can supply.
You can use RelativeSource AncestorType to bind to properties of the view itself:
<TextBlock FontSize="{Binding RelativeSource={RelativeSource AncestorType=Window},Path=MyFontSize}" />
Using ElementName should work as well:
<Window x:Name="window">
<TextBlock FontSize="{Binding ElementName=window,Path=MyFontSize}" />
</Window>
Edit
Here is an example that I've confirmed working:
XAML
<Window x:Class="WpfAbc.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
ToolTip="{Binding RelativeSource={RelativeSource Self},Path=MyToolTip}"
>
<Grid>
<TextBlock Text="hello world" FontSize="{Binding RelativeSource={RelativeSource AncestorType=Window},Path=MyFontSize}" />
</Grid>
</Window>
Code Behind
public partial class MainWindow : Window
{
private int m_fontSize = 20;
public int MyFontSize
{
get { return m_fontSize; }
set
{
if (m_fontSize != value)
{
m_fontSize = (value > 0) ? value : 10;
}
}
}
public string MyToolTip
{
get { return "hello world"; }
}
public MainWindow()
{
InitializeComponent();
}
}
Articles on this topic:
The RelativeSource markup extension
XAML binding declarations
Related background:
"Namescopes" in XAML (when binding to a source using "ElementName", the source element must be in the same namescope)
Visual tree vs logical tree in XAML (elements not in the visual tree, like Popup and ContextMenu, do not inherit DataContext. Binding from these elements requires a workaround like the "data context spy" technique.)
I'm working on a "Modern UI" application so the syntax is a bit new for me and I just can't seem to get my bindings to work properly.
My desire is to have a ViewModel first design so that on my apps pages I can just do things like have a ListView and add a UserViewModel as a child, and have the DataTemplate be found automatically to create a UserView and bind to the supplied UserViewModel.
I do something similar for a different app written for Win 7 desktop and it just works but for the life of me I can't figure out why it doesn't work here. I just get in my ListView "UserViewModel" as text (no UserControl created).
The only other difference here is it is the first time I'm using async functions since it pretty much is forced on you for Win 8 development, and that is the methods I get from the WCF service I'm pulling my data from.
Here's an example of my view model:
public class UserViewModel
{
private UserDTO _user { get; set; }
public UserViewModel(UserDTO user)
{
_user = user;
}
public UserViewModel(int userId)
{
SetUser(userId);
}
private async void SetUser(int userId)
{
ServiceClient proxy = new ServiceClient();
UserDTO referencedUser = await proxy.GetUserAsync(userId);
}
public string FirstName
{
get
{
return _user.FirstName;
}
}
public string LastName
{
get
{
return _user.LastName;
}
}
public string Email
{
get
{
return _user.email;
}
}
}
The view is supposed to be all XAML and glued together in the application resources as follows:
<UserControl x:Class="TaskClient.Views.UserView" ...
xmlns:root="using:TaskClient"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="30"
d:DesignWidth="200">
<StackPanel Orientation="Horizontal" Margin="5, 0, 0 ,0" DataContext="{Binding}">
<TextBlock x:Name="FirstNameLabel" Text="{Binding FirstName}"/>
<TextBlock x:Name="LastNameLabel" Text="{Binding LastName}"/>
<TextBlock x:Name="EmailLabel" Text="{Binding Email}"/>
</StackPanel>
</UserControl>
and :
<Application x:Class="TaskClient.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TaskClient"
xmlns:localData="using:TaskClient.Data"
xmlns:vm="using:ViewModels"
xmlns:vw="using:TaskClient.Views">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!--
Styles that define common aspects of the platform look and feel
Required by Visual Studio project and item templates
-->
<ResourceDictionary Source="Common/StandardStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Application-specific resources -->
<x:String x:Key="AppName">TaskClient</x:String>
<DataTemplate x:Key="vm:UserViewModel">
<vw:UserView />
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
I've tried searching for an hour or so now through various examples (eg. http://joshsmithonwpf.wordpress.com/a-guided-tour-of-wpf/) and haven't been able to find an example that works in my case.
Any idea what I'm doing wrong?
Could be a typo but you don't seem to update the "_user" field when you fetch a user via WCF service. You probably need to change this:
private async void SetUser(int userId)
{
ServiceClient proxy = new ServiceClient();
UserDTO referencedUser = await proxy.GetUserAsync(userId);
}
To this:
private async void SetUser(int userId)
{
ServiceClient proxy = new ServiceClient();
_user = await proxy.GetUserAsync(userId);
}
Also I don't see your ViewModel class implementing INotifyPropertyChange interface which is the key to WPF databinding. Once that's done and you have loaded a user, you need to notify WPF about properties being updated:
private async void SetUser(int userId)
{
ServiceClient proxy = new ServiceClient();
_user = await proxy.GetUserAsync(userId);
NotifyOfPropertyChange();
}
private void NotifyOfPropertyChange()
{
NotifyChanged("FirstName"); //This would raise PropertyChanged event.
NotifyChanged("LastName");
NotifyChanged("Email");
}
I just get in my ListView "UserViewModel" as text (no UserControl created)
Your DataTemplate needs be defined with the DataType property and no x:Key property, for the DataTemplate to get applied implicitly
<DataTemplate DataType="{x:Type vm:UserViewModel}">
<vw:UserView />
</DataTemplate>
A DataTemplate with a DataType specified but no x:Key is an Implicit DataTemplate, meaning it will be used implicitly anytime WPF needs to draw an object of the specified data type.
A DataTemplate with an x:Key property needs to actually be specified in your code by key, such as
<ListView ItemTemplate="{StaticResource MyKey}" ... />
Also, I'm not sure if the title of your question ("DataContext for MVVM: where does it go?") is a typo or not since your question body doesn't appear to be asking about the DataContext, however I have an beginners article on my blog explaining the DataContext that you may be interested in if you're struggling to understand the DataContext