Get Element Root's ViewModel Context in WINUI3 - xaml

I have a page which contains a ListView x:bound to an object in my ViewModel. This object contains a list of objects (timestamps) that contains a list of Subjects that contains a list of another objects. I'm presenting the data in 2 list views, one inside another.
<ListView
x:Name="primaryList" // for exemplification purposes
ItemsSource="{x:Bind ViewModel.VideoProject.TimeStamps, Mode=OneWay}"
ItemClick='{x:Bind ViewModel.ListViewTimeStamps_ItemClick, Mode=OneWay}'>
The ListView contains a DataTemplate for another ListView
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Spacing="5">
<TextBlock Text="{Binding Id}"
FontSize="15"
HorizontalAlignment="Left"
FontWeight="Bold" />
<ListView ItemsSource="{Binding Subjects}"
x:Name="secondaryList"
SelectionMode="Multiple">
<ListView.ItemTemplate>
....
And the second ListView is followed by another same structure.
My goal is to bind the second ListView ItemClickEvent to ListViewTimeStamps_ItemClick method inside my ViewModel, because I need the data contained in the object that secondaryListView holds (Subject).
I could try to set the Data Template Context to the ViewModel but it would break the Subject bind.
I found a ton of questions about this topic but differently from WPF there's not AncestorType to catch the up tree reference.
Obs:
My project is based on the Template Model which creates the XAML .cs with the ViewModel as a Property. I also haven't set the DataContext on the XAML page because I can x:bind normally my view model to the page elements without explicit set.
Is there a way to accomplish without using Attached Properties?
Thank you.

Since there is no support for setting the AncestorType property of a RelativeSource in WinUI, there is no way to accomplish this in pure XAML without writing some code.
You could implement an attached bevaiour as suggested and exemplified here:
public static class AncestorSource
{
public static readonly DependencyProperty AncestorTypeProperty =
DependencyProperty.RegisterAttached(
"AncestorType",
typeof(Type),
typeof(AncestorSource),
new PropertyMetadata(default(Type), OnAncestorTypeChanged)
);
public static void SetAncestorType(FrameworkElement element, Type value) =>
element.SetValue(AncestorTypeProperty, value);
public static Type GetAncestorType(FrameworkElement element) =>
(Type)element.GetValue(AncestorTypeProperty);
private static void OnAncestorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement target = (FrameworkElement)d;
if (target.IsLoaded)
SetDataContext(target);
else
target.Loaded += OnTargetLoaded;
}
private static void OnTargetLoaded(object sender, RoutedEventArgs e)
{
FrameworkElement target = (FrameworkElement)sender;
target.Loaded -= OnTargetLoaded;
SetDataContext(target);
}
private static void SetDataContext(FrameworkElement target)
{
Type ancestorType = GetAncestorType(target);
if (ancestorType != null)
target.DataContext = FindParent(target, ancestorType);
}
private static object FindParent(DependencyObject dependencyObject, Type ancestorType)
{
DependencyObject parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null)
return null;
if (ancestorType.IsAssignableFrom(parent.GetType()))
return parent;
return FindParent(parent, ancestorType);
}
}
So no replacement so far for AncestorType?
No. Not in XAML.

Related

UWP "compound" binding (binding Data.Name instead of Name)

I have a simple page with text block and a button. I want to change the text when I press the button. But using a text from a Data.Name property of the Page.
I know I can have this simpler (having just Name instead of Data.Name), but I need Data.Name, don't ask why.
For this I have a class DataType which has the Name property and object named Data of that class. I want to have Data inside this Page, and bind the text to the Data.Name property.
When I click on the button, nothing happens, the question is how canI make this work?
XAML:
<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Data.Name, Mode=OneWay}"/>
<Button Grid.Row="1" Content="Change" Click="Button_Click"/>
</Grid>
</Page>
Class DataType
public ref class DataType: public INotifyPropertyChanged {
public:
property String^ Name
{
String^ get() {
return m_Name;
}
void set(String^ value) {
m_Name = value;
OnPropertyChanged("Name");
}
}
virtual event PropertyChangedEventHandler^ PropertyChanged;
private:
void OnPropertyChanged(Platform::String^ propertyName)
{
PropertyChanged(this, ref new PropertyChangedEventArgs(propertyName));
}
String^ m_Name;
};
Class MainPage
public ref class MainPage sealed: public INotifyPropertyChanged
{
public:
MainPage();
property DataType^ Data {
DataType^ get() {
return m_Data;
}
void set(DataType^ value) {
m_Data = value;
OnPropertyChanged("Data");
}
}
virtual event PropertyChangedEventHandler^ PropertyChanged;
private:
void OnPropertyChanged(Platform::String^ propertyName)
{
PropertyChanged(this, ref new PropertyChangedEventArgs(propertyName));
}
void Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
{
Data->Name = rand() & 1 ? "Test1" : "Test2";
OnPropertyChanged("Data");
}
DataType^ m_Data;
};
In UWP, there are x:Bind and Binding markup extension, they have some differences when you use them. You can learn the details from the document of above two links.
Now we will discuss the reason that caused your above issue.
In your xaml, you use the Binding markup extension to bind the property path, since Binding uses the DataContext as a default source. Simplely to say, when you use Binding property path, you bind the DataContext.Property path, you just need use the Bind source object's property but not need to specify the Source data object on the xaml. As the introduction of Traversing an object graph:
"{Binding Path=Customer.Address.StreetAddress1}"
Here's how this path is evaluated:
The data context object (or a Source specified by the same Binding) is searched for a property named "Customer".
The object that is the value of the "Customer" property is searched for a property named "Address".
The object that is the value of the "Address" property is searched for a property named "StreetAddress1".
See the Property-path syntax for the details.
So your code will work just binding the Name property and set the DataContext. (Note that: your MainPage class don't need to implement the INotifyPropertyChanged interface.)
<TextBlock Grid.Row="0" Text="{Binding Name, Mode=OneWay}"/
And
this->DataContext = Data;
Also note: If you're using Visual C++ component extensions (C++/CX) then, because we'll be using {Binding}, you'll need to add the BindableAttribute attribute to the DataType class.
[Windows::UI::Xaml::Data::Bindable]
public ref class DataType sealed : public INotifyPropertyChanged {
...
}
On the other hand, you can use the x:Bind instead of the Binding, since x:Bind don't use the DataContext as a default source—instead, it uses the page or user control itself. So it will look in the code-behind of your page or user control for properties, fields, and methods. To expose your view model to {x:Bind}, you will typically want to add new fields or properties to the code behind for your page or user control. For example: in a page, Text="{x:Bind Employee.FirstName}" will look for an Employee member on the page and then a FirstName member on the object returned by Employee.
The issue is that DataType properties isn't visible from XAML, because "DataType.h" isn't included in "pch.h"
Once I included it in precompiled header, everything worked.
BTW, looks like Binding is not checking for type visibility from XAML or whatever, but x:Bind does. Using x:Bind, the compiler complains about unknown Data.Name, and that allowed me to figure out the problem.

Why I get exception when setting ComboBox.SelectedIndex?

I am trying to learn about bindings and XAML. I have a very simple example, where I bind an array of strings to a combobox defined in XAML. I also want to set the selected index.
I am getting an exception:
Value does not fall within the expected range.
with the SelectedIndex property.
Here my XAML for a UWP application.
<StackPanel Background="{ThemeResource applicationPageBackgroundThemeBrush}">
<ComboBox Name="BrowserCombo" ItemsSource="{x:Bind ComboStrings}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
and the code behind
public sealed partial class MainPage : Page
{
private string[] comboStrings = { "One", "Two", "Three" };
public List<String> ComboStrings
{
get { return new List<string>(comboStrings); }
}
public MainPage()
{
this.InitializeComponent();
DataContext = this;
BrowserCombo.SelectedIndex = 1;
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// BrowserCombo.SelectedIndex = 1;
}
}
I want to have it very simple, so no MVVM - I still need to learn this concept.
Remarks:
I can put SelectedIndex in the XAML file. Same problem.
If I put the SelectedIndex in the Page_Loaded event handler, it works fine.
In a classic desktop XAML (aka WPF) there will be no problem.
It seems to be that the item list will be populated differently between XAML-WinRt and XAML-WPF. What is the earliest point to access the SelectedIndex property?
You get this exception because your ItemsSource is still null when you try to set BrowserCombo.SelectedIndex = 1; in the page's constructor. You are using x:Bind - if you debug the generated MainPage.g.cs file and put some breakpoints there:
public void Loading(global::Windows.UI.Xaml.FrameworkElement src, object data)
{
this.Initialize();
}
public void Initialize()
{
if (!this.initialized)
{
this.Update();
}
}
public void Update()
{
this.Update_(this.dataRoot, NOT_PHASED);
this.initialized = true;
}
You will see that x:Bind is being resolved in Loading event - this explains why in constructor you still have null in ItemsSource.
The other thing is that with old Binding - it is resolved when you set DataContext = this;, but you are using x:Bind and in fact you don't need to set the DataContext - it doesn't change anything here. If you replace ItemsSource="{x:Bind ComboStrings}" with ItemsSource="{Binding ComboStrings}" then your code should work, otherwise you may remove the line DataContext = this;.
Another interesting thing is that the order of defined bindings in XAML, can also cause similar troubles - for example if you define your ComboBox like this:
<ComboBox Name="BrowserCombo" SelectedIndex="{x:Bind Index}" ItemsSource="{x:Bind ComboStrings}">
then you will also get exception - SelectedIndex is being resolved before the collection is set up. Opposite to the situation when it works fine:
<ComboBox Name="BrowserCombo" ItemsSource="{x:Bind ComboStrings}" SelectedIndex="{x:Bind Index}">

Command parameter in EventTrigger

I'm working on WinRT with MvmmCross v3 framework and Windows.UI.Interactivity.
I want a TextBox with an EventTrigger on the event TextChanged which launch a Command. And also, I want in CommandParameter the text of the textBox.
So I have this code
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding UpdateText}" CommandParameter="{Binding Text}"/>
</i:EventTrigger>
public ICommand UpdateText
{
get
{
return new MvxCommand<object>((textSearch) =>
{
// code...
});
}
}
But my textSearch parameter equals to "{Windows.UI.Xaml.Controls.TextChangedEventArgs}" with all of these properties NULL.
I Tried also to declare explicitly my ElementName in the binding like
CommandParameter="{Binding Path=Text, ElementName=tes}
But it failed too.
Thanks
Do you really need to handle TextChanged event? You could be notified of the changes by just binding to the Text property:
<TextBox Text="{Binding TextValue, Mode=TwoWay}" />
And then in the view model:
private string _textValue;
public string TextValue
{
get
{
return _textValue;
}
set
{
if (_textValue == value)
return;
_textValue = value;
OnTextChanged(value); // react to the changed value
}
}
EDIT:
There are two things you need to be aware of, if you want to get to the Text value from inside your Command:
First, you need to fix the CommandParameter binding. By using {Binding Text} you are actually trying to bind to a property in your view model, i.e. you would first need to bind the TextBox.Text property to the same view model property. As you've said in the comment, that's no good for you because you need the info on every change and not only on lost focus, so the value you get is not up to date enough.
The right approach would therefore be your second attempt, i.e. binding directly to the TextBox using the ElementName syntax. Unfortunately triggers are not a part of the visual tree therefore they don't get access to the XAML name scope (you can read more about it in this blog post). You can work around that by using NameScopeBinding from MVVMHelpers.Metro package:
<Behaviors:NameScopeBinding x:Key="MyTextBox"
Source="{Binding ElementName=MyTextBox}" />
Now you can make the ElementName binding work:
<i:InvokeCommandAction Command="{Binding UpdateText}"
CommandParameter="{Binding Source.Text, Source={StaticResource MyTextBox}}"/>
You still have the second problem. The Text value that you are binding to only updates on lost focus so you don't get the right value when handling TextChanged event. The solution is to bind to the TextBox itself:
<i:InvokeCommandAction Command="{Binding UpdateText}"
CommandParameter="{Binding Source, Source={StaticResource MyTextBox}}"/>
And then inside the command get the Text property directly from the TextBox:
private void OnUpdateText(object parameter)
{
var value = ((TextBox) parameter).Text;
// process the Text value as you need to.
}
EDIT 2:
To make the above code work with the view model being in a PCL, there a couple of approaches you could take.
The simplest hack would be to use reflection. Since it is available in PCL you could get to the Text property and read its value:
private void OnUpdateText(object parameter)
{
var textProperty = textSearch.GetType().GetProperty("Text");
var value = textProperty.GetValue(parameter, null) as string;
// process the Text value as you need to.
}
A cleaner solution would be to put the WinRT specific code into a separate assembly abstracted via an interface. You would first create an interface in the PCL library:
public interface IUiService
{
string GetTextBoxText(object textBox);
}
And modify view model to accept this interface in the constructor:
public ViewModel(IUiService uiService)
{
_uiService = uiService;
}
In the command handler you would than use the method in the interface:
private void OnUpdateText(object parameter)
{
var value = _uiService.GetTextBoxText(parameter);
// process the Text value as you need to.
}
You would implement that interface in a WinRT library:
public class UiService : IUiService
{
public string GetTextBoxText(object textBox)
{
var typedTextBox = textBox as TextBox;
return typedTextBox.text;
}
}
In the application you reference this library and pass the implementation to view model:
var viewModel = new ViewModel(new UiService);
My favorite approach is different: I'd create a Behavior exposing a Text property automatically updated every time TextChanged event is triggered:
public class TextChangedBehavior : Behavior<TextBox>
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text",
typeof(string),
typeof(TextChangedBehavior),
new PropertyMetadata(null));
public string Text
{
get { return (string) GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TextChanged += OnTextChanged;
Text = AssociatedObject.Text;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.TextChanged -= OnTextChanged;
}
private void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
{
Text = AssociatedObject.Text;
}
}
Now I could bind a TextValue property to this behavior and react to its change in the property setter as already suggested at the very beginning of this long answer:
<TextBox>
<i:Interaction.Behaviors>
<b:TextChangedBehavior Text="{Binding TextValue, Mode=TwoWay}" />
</i:Interaction.Behaviors>
</TextBox>

Problems with SelectedValue, SelectedIndex while data binding to a ComboBox c++/cx XAML Metro app

How can I use the value of the ComboBox's selected element in the following code?
C++:
namespace testtesttest
{
[Windows::UI::Xaml::Data::Bindable]
public ref class Wrapper sealed : Windows::UI::Xaml::Data::INotifyPropertyChanged
{
public:
Wrapper()
{
// the index of the selected element of the combobox when the application starts
m_selectedElement = 2;
m_myStringArray = ref new Platform::Collections::Vector<int>(3);
// 1, 2, and 4 in the combobox list
m_myStringArray->SetAt(0,1);
m_myStringArray->SetAt(1,2);
m_myStringArray->SetAt(2,4);
}
virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged;
property Windows::Foundation::Collections::IVector<int>^ MyStringArray
{
Windows::Foundation::Collections::IVector<int>^ get() { return m_myStringArray; }
}
property int SelectedElement
{
int get() { return m_selectedElement; }
void set(int value) { m_selectedElement = value; RaisePropertyChanged("SelectedElement"); }
}
protected:
void RaisePropertyChanged(Platform::String^ propertyName)
{
PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(propertyName));
}
private:
Platform::Collections::Vector<int>^ m_myStringArray;
int m_selectedElement;
};
}
XAML:
<TextBlock HorizontalAlignment="Left"
Height="73" Margin="50,436,0,0"
TextWrapping="Wrap"
Text="{Binding Path=SelectedElement}"
VerticalAlignment="Top"
Width="200"/>
<ComboBox ItemsSource="{Binding Path=MyStringArray}"
SelectedIndex="{Binding Path=SelectedElement}"
HorizontalAlignment="Left"
Height="50" Margin="369,50,0,0"
VerticalAlignment="Top" Width="286"/>
I tested other bindings and they worked. I am setting the DataContext right.
The m_selectedElement = 2 in the constructor sets the selected element in the combobox to the 3rd in the list. The get() method of the SelectedElement property gets called, but the set() method doesn't. I checked this by placing a breakpoint. What am I doing wrong?
Also, is it possible to bind a Platform::Array^ to a ComboBox?
I tried using Platform::Array < Platform::String ^>^ and also Platform::Array < int>^ and I couldn't get it work. STL containers also didn't work. What are the other possible containers that can bind to a combobox?
Change
SelectedIndex="{Binding Path=SelectedElement}"
to
SelectedIndex="{Binding Path=SelectedElement, Mode=TwoWay}"
You need a two-way binding if you want the UI to update your ViewModel.
You can only use WinRT components in bindings (ref classes/structs, enum classes). Using Platform::Collections::Vector is generally the right choice when it's used for binding, especially because it also implements IObservableVector. STL containers don't work because they cannot travel across the ABI.

Is there something similar to TabControlRegionAdapter.ItemContainerStyle Attached Property for ItemsControl?

I'm using Prism 4 with Silverlight and I want to use ItemsControl to host multiple views. I really want all the views to be wrapped inside a specified ItemTemplate or be able to specify an ItemStyle so that I can use something like the Expander control in the Silverlight Toolkit . When I try to specify an ItemTemplate an unhandled System.NotSupportedException is thrown at runtime.
ItemsControl.Items must not be a UIElement type when an ItemTemplate is set.
at System.Windows.Controls.ItemsControl.MS.Internal.Controls.IGeneratorHost.GetContainerForItem(Object item, DependencyObject recycledContainer)
at System.Windows.Controls.ItemContainerGenerator.Generator.GenerateNext(Boolean stopAtRealized, Boolean& isNewlyRealized)
at System.Windows.Controls.ItemContainerGenerator.System.Windows.Controls.Primitives.IItemContainerGenerator.GenerateNext(Boolean& isNewlyRealized)
at System.Windows.Controls.ItemsControl.AddContainers()
at System.Windows.Controls.ItemsControl.RecreateVisualChildren(IntPtr unmanagedObj)
Code
<ItemsControl Regions:RegionManager.RegionName="DetailsRegion">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Red" BorderThickness="1">
<ContentPresenter Content="{Binding}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
It has been a while since I used PRISM, but the following is an example that you can use to implement a custom IRegion that wraps the element before adding it to the collection.
public class RegionWrapper : Region
{
public override Microsoft.Practices.Composite.Regions.IRegionManager Add(object view, string viewName, bool createRegionManagerScope)
{
var myWrapper = new Wrapper();
myWrapper.Content = view;
return base.Add(myWrapper, viewName, createRegionManagerScope);
}
}
To register this item you need to create a Region factory, which in PRISM they call an adapter
public class RegionWrapperAdapter : RegionAdapterBase<IRegionAdapter>
{
protected override Microsoft.Practices.Composite.Regions.IRegion CreateRegion()
{
return new RegionWrapper();
}
}
Then on your Bootstrap just register your adapter
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var regionAdapterMappings = base.ConfigureRegionAdapterMappings();
regionAdapterMappings.RegisterMapping(typeof(ItemsControl), Container.Resolve<RegionWrapperAdapter>());
return regionAdapterMappings;
}
Of course the other part left is to implement the control 'Wrapper' so you can create that class and add the content. It could simply be a ContentControl with a particular style similar to what you have in this example or add anything fancier.
This code is based on an old version of PRISM, so things might have changed recently.
Hope this helps
Miguel