How do I assign a Converter from view class to Resource? - c#

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.
}

Related

Binding MAUI ControlTemplate to ContentPage view model

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).

How to customize UI or DataTemplate for each item in listView UWP C#

I have a listView containing different types of items and I need to display them using different elements in UI. e.g. i have children and adult members in listView, and children will not have kids, spouses etc, while adults will have their children, spouses, workplace etc. As far as i know, once i layout structure in XAML using data template, i cannot change it. I created a UserControl for different items, not sure how to use it in ListView when adding items.
Looking for help on how to do this.
Thanks in advance.
Based on your scenario, you could try to use DataTemplateSelector Class. This class enables you to apply different templates for ListView based on your own logic.
Here are the steps that you need to do to implement this:
You will need to create your own DataTemplateSelector Class. Then you could declare each template as a property of the class.
You need to create an instance of your own DataTemplateSelector class in the Resources section of your XAML file. You should create instances of DataTemplate objects and define their layout in the resources section. Then assign these data templates to the template properties you declared in the DataTemplateSelector class.
The final step is that assign the DataTemplateSelector class to the ItemTemplateSelector property of the ListView.
I've made a simple demo and you could refer to the following code:
Code behind:
public sealed partial class MainPage : Page
{
public List<int> NumbersList { get; set; }
public MainPage()
{
this.InitializeComponent();
NumbersList = new List<int>();
for (int i=0;i<10; i++)
{
NumbersList.Add(i);
}
}
}
public class MyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate ChildrenTemplate { get; set; }
public DataTemplate AdultTemplateent { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
// use your own conditions
if ((int)item % 2 == 0)
{
return AdultTemplateent;
}
else
{
return ChildrenTemplate;
}
}
}
Xaml:
<Page.Resources>
<DataTemplate x:Key="AdultTemplateent" x:DataType="x:Int32">
<StackPanel Orientation="Horizontal" Background="LightGray">
<TextBlock Text="This is Adult Item" Margin="5"/>
<TextBlock Text="{x:Bind}" Margin="5"/>
<TextBlock Text="Workplace:NewYork" Margin="5"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ChildrenTemplate" x:DataType="x:Int32">
<StackPanel Orientation="Vertical" Background="LightBlue">
<TextBlock Text="This is Children Item" Margin="5" />
<TextBlock Text="{x:Bind}" Margin="5" />
<TextBlock Text="School:DC" Margin="5"/>
</StackPanel>
</DataTemplate>
<local:MyDataTemplateSelector x:Key="MyDataTemplateSelector" AdultTemplateent="{StaticResource AdultTemplateent}" ChildrenTemplate="{StaticResource ChildrenTemplate}"/>
</Page.Resources>
<Grid>
<ListView x:Name = "TestListView"
ItemsSource = "{x:Bind NumbersList}"
ItemTemplateSelector = "{StaticResource MyDataTemplateSelector}">
</ListView>
</Grid>
How it looks like:
You could get more detailed information here: Data template selection: Styling items based on their properties

How can I display view inside content control in wpf

FirstView.xaml contains Something like this
<ContentControl Name="ContentControlName" Grid.Row="1" Grid.Column="0" Content="{Binding SelectionViewModel}"/>
My SelectionView.xaml contains
<TextBox x:Name="textBoxName" Text="{Binding Name}"/>
<TextBox IsReadOnly="True" Text="{Binding Uid}"/>
In the FirstViewModel I have created a property like below
private SelectionViewModel selectionViewModel;
public SelectionViewModel SelectionViewModel
{
get
{
return this.selectionViewModel;
}
}
Content control with two text box is not displayed when I run
Is the way done right?
Since you used binding, you need raise up PropertyChanged event.
Your ViewModel class (SelectionViewModel) must implement INotifyPropertyChanged.
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private SelectionViewModel selectionViewModel;
public SelectionViewModel SelectionViewModel
{
get
{
return this.selectionViewModel;
}
private set
{
this.selectionViewModel = value;
PropertyChanged?.Invoke(this, nameof(SelectionViewModel));
}
}
}
You passed SelectionViewModel instance to Content property of ContentControl.
Your ContentControl must have special datatemplate coupled with this view model. Otherwise, it will not work.
For example:
<ContentControl Content="{Binding SelectionViewModel}">
<ContentControl.ContentTemplate>
<DataTemplate DataType="{x:Type local:SelectionViewModel}">
<!-- Here is your template -->
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
Also you shouldn't use same names for type SelectionViewModel and property SelectionViewModel.
Since you wasn't provide a source code we can't figure out the exact cause of your error.
I hope it was helpful for you.

x:Bind ViewModel method to an Event inside DataTemplate

I'm basically asking the same question as this person, but in the context of the newer x:Bind.
ViewModels' DataContext is defined like so
<Page.DataContext>
<vm:ChapterPageViewModel x:Name="ViewModel" />
</Page.DataContext>
So whenever I need to bind something I do it explicitely to the ViewModel like so
ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}"
However that doesn't work within templates
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}"> <-- this here is the culprit
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
Reading the documentation, I found that using Path should basically reset the context to the page, but this (x:Bind Path=ViewModel.PageResizeEvent didn't work either. I'm still getting Object reference not set to an instance of an object, which should mean that it doesn't see the method (but a null).
Image class:
public class Image {
public int page { get; set; }
public string url { get; set; }
public int width { get; set; }
public int heigth { get; set; }
}
And in the ChapterPageViewModel
private List<Image> _pageList;
public List<Image> pageList {
get { return _pageList; }
set { Set(ref _pageList, value); }
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode,
IDictionary<string, object> suspensionState)
{
Initialize();
await Task.CompletedTask;
}
private async void Initialize()
{
pageList = await ComicChapterGet.GetAsync(_chapterId);
}
public void PageResized(object sender, SizeChangedEventArgs e)
{
//resizing logic happens here
}
We have two problems here:
First, trying to directly bind an event to a event handler delegate
That will never work, simply put.
One way to handle an event on MVVM pattern is by using EventTrigger and ICommand.
It requires a class that implements ICommand. This post will help you if don't know how to do it. I'll call mine DelegateCommand.
Here's how I would refactor it in two steps:
1) Add a Command to the VM:
public class ChapterPageViewModel
{
public ChapterPageViewModel()
{
this.PageResizedCommand = new DelegateCommand(OnPageResized);
}
public DelegateCommand PageResizedCommand { get; }
private void OnPageResized()
{ }
}
2) Bind that Command to the SizeChanged event with EventTrigger and InvokeCommandAction.
<Page (...)
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core">
(...)
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" >
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer>
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SizeChanged">
<core:InvokeCommandAction
Command="{x:Bind ViewModel.PageResizedCommand }" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
</Page>
"But Gabriel", you say, "that didn't work!"
I know! And that's because of the second problem, which is trying to x:Bind a property that does not belong to the DataTemplate class
This one is closely related to this question, so I´ll borrow some info from there.
From MSDN, regarding DataTemplate and x:Bind
Inside a DataTemplate (whether used as an item template, a content
template, or a header template), the value of Path is not interpreted
in the context of the page, but in the context of the data object
being templated. So that its bindings can be validated (and efficient
code generated for them) at compile-time, a DataTemplate needs to
declare the type of its data object using x:DataType.
So, when you do <ScrollViewer SizeChanged="{x:Bind ViewModel.PageResized}">, you're actually searching for a property named ViewModel on the that models:Image class, which is the DataTemplate's x:DataType. And such a property does not exist on that class.
Here, I can see two options. Choose one of them:
Add that ViewModel as a property on the Image class, and fill it up on the VM.
public class Image {
(...)
public ChapterPageViewModel ViewModel { get; set; }
}
public class ChapterPageViewModel
{
(...)
private async void Initialize() {
pageList = await ComicChapterGet.GetAsync(_chapterId);
foreach(Image img in pageList)
img.ViewModel = this;
}
}
With only this, that previous code should work with no need to change anything else.
Drop that x:Bind and go back to good ol'Binding with ElementName.
<FlipView ItemsSource="{x:Bind ViewModel.pageList, Mode=OneWay}" x:Name="flipView">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:Image">
<ScrollViewer>
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SizeChanged">
<core:InvokeCommandAction
Command="{Binding DataContext.PageResizedCommand
, ElementName=flipView}" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{x:Bind url}"/>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
This one kind of defeat the purpose of your question, but it does work and it's easier to pull off then the previous one.

Attach ICommand in WPF UserControl

I implemented a simple button with an image in it:
<Button Command="{Binding ButtonCommand, ElementName=ImageButtonControl}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ButtonImage, ElementName=ImageButtonControl}"/>
<TextBlock Text="{Binding ButtonText, ElementName=ImageButtonControl}" Margin="2,0,0,0"/>
</StackPanel>
</Button>
As you can see, I expose a ButtonCommand property in order to be able to attach an ICommand to this UserControl:
public partial class ImageButton : UserControl
{
/// <summary>
/// The dependency property that gets or sets the source of the image to render.
/// </summary>
public static DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ButtonImage", typeof(ImageSource), typeof(ImageButton));
public static DependencyProperty TextProperty =
DependencyProperty.Register("ButtonText", typeof(string), typeof(ImageButton));
public static DependencyProperty ButtonCommandProperty =
DependencyProperty.Register("ButtonCommand", typeof(ICommand), typeof(ImageButton));
public ImageButton()
{
this.DataContext = this;
InitializeComponent();
}
/// <summary>
/// Gets or sets the button command.
/// </summary>
public ICommand ButtonCommand
{
get { return (ICommand)GetValue(ImageButton.ButtonCommandProperty); }
set { SetValue(ImageButton.ButtonCommandProperty, value); }
}
/// <summary>
/// Gets or sets the button image.
/// </summary>
public ImageSource ButtonImage
{
get { return (ImageSource)GetValue(ImageButton.ImageSourceProperty); }
set { SetValue(ImageButton.ImageSourceProperty, value); }
}
/// <summary>
/// Gets or sets the button text.
/// </summary>
public string ButtonText
{
get { return (string)GetValue(ImageButton.TextProperty); }
set { SetValue(ImageButton.TextProperty, value); }
}
}
Then when I declare my button it gives this:
<uc:ImageButton Grid.Row="1" Grid.Column="0" ButtonCommand="{Binding AttachContextCommand}" ButtonImage="{StaticResource AssociateImage}" ButtonText="Associer"/>
And badaboom, nothing never happen when I click on my ImageButton.
When I replace the ImageButton with a simple button, the ICommand is called.
I even tried to simply extends the Button class and bind an ICommand, but once again, it didn't work...
Help appreciated !
Thx.
You can achieve this in a much cleaner way using a style and a couple of attached properties.
The attached properties will store your specific information.
The style will use these properties and build the look you want.
The element will still be a button so the command and everything else will work.
public class ImageButton
{
public static ImageSource GetImage(DependencyObject obj)
{
return (ImageSource)obj.GetValue(ImageProperty);
}
public static void SetImage(DependencyObject obj, ImageSource value)
{
obj.SetValue(ImageProperty, value);
}
public static readonly DependencyProperty ImageProperty =
DependencyProperty.RegisterAttached("Image", typeof(ImageSource), typeof(ImageButton), new UIPropertyMetadata(null));
public static String GetCaption(DependencyObject obj)
{
return (String)obj.GetValue(CaptionProperty);
}
public static void SetCaption(DependencyObject obj, String value)
{
obj.SetValue(CaptionProperty, value);
}
public static readonly DependencyProperty CaptionProperty =
DependencyProperty.RegisterAttached("Caption", typeof(String), typeof(ImageButton), new UIPropertyMetadata(null));
}
<Style TargetType="{x:Type Button}"
x:Key="ImageButton">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=(local:ImageButton.Image), RelativeSource={RelativeSource AncestorType={x:Type Button}}}" />
<TextBlock Text="{Binding Path=(local:ImageButton.Caption), RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Margin="2,0,0,0" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
You can then use this to declare buttons:
<Button Style="{DynamicResource ImageButton}"
local:ImageButton.Caption="Foo"
local:ImageButton.Image="..." />
Note:
I'm pretty sure it would be cleaner to go through the "Template" property and use a ControlTemplate and TemplateBindings, but that would mean re-creating the border and other stuff around your content, so if you are looking to just define a default "Content", my example would be the way to go, I think.
If the only added functionality that you want for your button is to have an image on it, then I think you're approaching this from the wrong direction. WPF is as awesome as it is because the UI controls are look-less. This means that a Control is merely a definition of functionality + some template to define how it looks. This means that the template can be swapped out at any time to change the look. Also, almost any content can be placed inside of almost any control
For instance, to define a button in your xaml that has the look your going for all you need is this:
<Window ...>
...
<Button Command="{Binding AttachContextCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource AssociateImage}"/>
<TextBlock Text="Associer"/>
</StackPanel>
</Button>
...
</Window>
Just keep in mind that with WPF you don't have to define a new CustomControl or UserControl every time you want to change the look and feel of something. The only time you should need a CustomControl is if you want to add functionality to an existing Control or to create functionality that doesn't exist in any other Control.
Edit Due to comment:
If you're wanting to keep from defining the content for the button every time, the other option is to just have a poco (plain old CLR object) class that would define everything your interested in (I'll write my example as if you're doing this for a tool bar, because it makes sense to me):
public class ToolBarItem : INotifyPropertyChanged
{
public string Text { get ... set ... }
public ICommand Command { get ... set ... }
public ImageSource Image { get ... set ... }
}
That has a data template defined somewhere (App.xaml, Window.Resources, etc):
<DataTemplate DataType="{x:Type l:ToolBarItem}">
<Button Command="{Binding Command}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Image}"/>
<TextBlock Text="{Binding Text}"/>
</StackPanel>
</Button>
</DataTemplate>
And then use the guy in your xaml like this:
<Window ...>
...
<ContentControl>
<ContentControl.Content>
<l:ToolBarItem Image="..." Command="..." Text="..."/>
</ContentControl.Content>
</ContentControl>
...
</Window>
I just don't know that the way you're trying to do it is the most WPF way you could do it.
EDIT Updated based on second comment
Sorry, I forgot to include the ContentControl surrounding that. Now that I remembered that, I realize that that's not much less verbose than the original where you are specifying the content manually. I'll post a new answer to help with your original question.
To re-answer the original question:
What I think you want to do is create a new CustomControl called ImageButton. Then change it to extend from Button instead of Control. You won't need a Command property since Button already has one. You'll only need to add an Image property and you can reuse the Content property from button instead of having a Text property.
When your CustomControl is created, it'll add an entry in your Generic.xaml for the default style of your ImageButton. In the Setter for the Template property you can change the ControlTemplate to this:
<ControlTemplate TargetType="{x:Type local:ImageButton}">
<StackPanel Orientation="Horizontal">
<Image Source="{TemplateBinding Image}"/>
<ContentPresenter/>
</StackPanel>
</ControlTemplate>
Then, again, when you want to use it:
<Window ... >
...
<l:ImageButton Image="{StaticResource ...}" Command="...">
Associer
</l:ImageButton>
...
</Window>
The built-in WPF button contains code that fires the attached command in response to being clicked. Your "ImageButton" derives from UserControl, so you don't get that behavior. Probably the shortest route to get what you want is for your ImageButton class to actually derive from the WPF Button class. To accomplish that, change the markup for ImageButton from
<UserControl
...
>
...
</UserControl>
to
<Button
...
>
...
</Button>
Then change the base class of ImageButton from UserControl to Button.
You'll probably need to make some other minor changes before it all works.

Categories

Resources