Can someone please explain what's going on here? I'm new to WPF and migrating my Forms project to WPF with binding. I'm using AvalonDock but I'm not binding directly to any of the AvalonDock controls. Here's a couple excerpts. I removed a lot for brevity's sake but let me know if you need to see something else.
EDIT: These two StackPanels are just tests... trying to figure this stuff out.
EDIT2: I'm trying to do MVVM eventually; I just need to get a better handle on binding so I know how to structure it.
EDIT3: See bottom of post.
Q: The first StackPanel does not update at all, never mind updating after changes. I've tried setting the DataContext in the StackPanel, Grid and TextBlock. What am I doing wrong?
Q: The second works fine when the parent grid is bound in code behind but only if bound where you see it, not in the MainWindow_Loaded() method. What's different here?
I've read several tutorials as well as plenty of similar questions here but nothing's helping me understand what the difference is here and what I'm missing.
<Window x:Class="TestUIWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ad="http://schemas.xceed.com/wpf/xaml/avalondock"
Title="MainWindow" Height="768" Width="1024"
Loaded="MainWindow_Loaded"
xmlns:vm="clr-namespace:TestUIWPF.ViewModel"
>
<!-- lots excluded for brevity. there are no Window.Resources -->
<ad:LayoutAnchorable Title="Test" >
<Grid x:Name="gridTest">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<StackPanel.DataContext>
<vm:EntityViewModel />
</StackPanel.DataContext>
<TextBlock Text="Label" />
<TextBlock DataContext="{Binding ActiveEntity}" Text="{Binding Path=Label}" />
</StackPanel>
<StackPanel Orientation="Horizontal" >
<TextBlock Text="Label Again" />
<TextBlock Text="{Binding Path=Label}" />
</StackPanel>
</StackPanel>
</Grid>
</ad:LayoutAnchorable>
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = this;
SelectedEntityViewModel = new ViewModel.EntityViewModel();
ImportEntityXML_Click(null, null); //skips clicking the menus
}
private void ImportEntityXML_Click(object sender, RoutedEventArgs e)
{
//omitted OpenFileDialog and XmlReader stuff
xmlreader = new XmlReader(dlg.FileName);
Entities.Add(xmlreader.ReadEntityFromXML());
SimulatedEntitySelection(Entities.ElementAt(0)); //haven't built any of the UI stuff for this yet
}
private void SimulatedEntitySelection(Entity ent)
{
SelectedEntityViewModel.ActiveEntity = ent;
gridTest.DataContext = SelectedEntityViewModel.ActiveEntity;
}
private void button_Click(object sender, RoutedEventArgs e)
{
SelectedEntityViewModel.ActiveEntity.Label = "test";
}
Entity and EntityViewModel implement INotifyPropertyChanged and it works just fine with the second StackPanel. The button that calls button_Click() is just for testing the binding. EntityViewModel pretty much just wraps Entity through the ActiveEntity property and helps with reading the collections-of-collections within Entity.
EDIT3:
I've also tried a couple resources. Here's how I did the ObjectDataProvider:
<Window.Resources>
<ObjectDataProvider x:Key="testVM" ObjectType="{x:Type vm:EntityViewModel}" />
<vm:EntityViewModel x:Key="SelEntVM" />
</Window.Resources>
<!-- .... -->
<StackPanel.DataContext>
<Binding Source="{StaticResource testVM}" />
</StackPanel.DataContext>
<TextBlock Text="Label" />
<TextBlock Text="{Binding Path=ActiveEntity.Label}" />
Your first stack panel is not working, because the data context is inherited. Therefore, once you change the DataContext of the Grid to the ActiveEntity object, the binding on the text block in the first data context will set the datacontext for the TextBlock to the ActiveEntity on the current datacontext, which would already be the ActiveEntity (therefore ActiveEntity.ActiveEntity) And than try to bind to the Label property on that. E.g. ActiveEntity.ActiveEntity.Label
Before the click, you are setting the DataContext of that window to "this" which I am assuming is not the ViewModel, it is the code behind?
If you are using MVVM,
you should have something like this
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
SelectedEntityViewModel = new ViewModel.EntityViewModel();
this.DataContext = SelectedEntityViewModel;
ImportEntityXML_Click(null, null); //skips clicking the menus
}
Or some other ViewModel which provides all the necessary data.
You nomrally would have a MainWindowView and MainWindowViewModel, at least that is the convention and usually you set the datacontext of the window in the constructor once(you can do it in the Loaded handler), in most cases you shouldn't need to manually change the DataContext of any framework elements in the code behind.
EDIT: Example Code:
MainWindow.xaml
<Window x:Class="SO27760357.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="300">
<Grid>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Label" />
<TextBlock Text="{Binding ActiveEntity.Label}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Label Again" />
<TextBlock Text="{Binding ActiveEntity.Label}" />
</StackPanel>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
MainWindowViewModel.cs (INotifyPropertChanged omitted for simplicity)
public class MainWindowViewModel
{
public EntityViewModel ActiveEntity { get; set; }
}
EntityViewModel.cs (INotifyPropertChanged omitted for simplicity)
public class EntityViewModel
{
public string Label { get; set; }
}
As you can see, I am setting the DataContext of the Window to the MainViewModel, therefore the DataContext(root of all bindings) is the MainViewModel, and each TextBlock needs to first access the ActiveEntity property first, before it can get to the Label proeprty.
THe other option is, that if everything inside the main stack panel you given us, will be bound to ActiveEntity, you can change the DataContext of that StackPanel, binding it to the ActiveEntity, and therefore all of its children datacontext will also be that object.
<StackPanel Orientation="Vertical"
**DataContext="{Binding ActiveEntity}"**>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Label" />
<TextBlock **Text="{Binding Label}"**/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Label Again" />
<TextBlock **Text="{Binding Label}"** />
</StackPanel>
</StackPanel>
EDIT 2 - Advice
You should refrain from referencing objects by name as much as possible, and have as little logic in the code behind as possible, if any. For most simple screens there is no need to have anything in code behind other than the initial Binding of the DataContext (if you don't have a window service which creates + sets the DataContext of windows)
It does work. You might be probably updating a wrong viewmodel.
Once you define the viewmodel in DataContext, youd have to access it this way:
private void button_Click(object sender, RoutedEventArgs e)
{
var myModel = (ViewModel.EntityViewModel)(yourStackPanelName.DataContext);
myModel.ActiveEntity.Label = "test";
}
Related
On Uno Platform project, using several data templates for TabViewItem, TreeViewItem and ListViewItem, trying to fire commands via various events, like ItemInvoked (TreeView), DoubleTapped (ListView), CloseRequested (TabView).
Strangely enough it works for few invocations, then it stops. Note that (also the same) commands bound to Buttons via their Command binding continue working.
Example of TabView close attempt. Typically works for first 3 to 5 tabs:
<DataTemplate x:Key="HtmlFileTemplate" x:DataType="local:FileContentViewModel">
<TabViewItem Header="{x:Bind Info.Name}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind Content}" />
<!--
This just works:
-->
<Button Command="{x:Bind CloseCommand}">Invoke FileContentViewModel.CloseCommand</Button>
</StackPanel>
<!--
This stops working after few invocations
(typically together with all other Interaction.Behaviors bindings):
-->
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="CloseRequested">
<ic:InvokeCommandAction Command="{x:Bind CloseCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
</TabViewItem>
</DataTemplate>
<local:TabsTemplateSelector x:Key="TabsTemplateSelector" HtmlFileTemplate="{StaticResource HtmlFileTemplate}" ... />
...
<TabView TabItemsSource="{x:Bind ViewModel.Tabs}" TabItemTemplateSelector="{StaticResource TabsTemplateSelector}">
</TabView>
I hope, there is some flagrant issue in my usage I can't simply see. Any Help appreciated. Using latest uno stuff (WinUI, dotnet6), debugging on Windows head:
dotnet new unoapp -o UnoWinUI3AppName
<PackageReference Include="CommunityToolkit.Mvvm" Version="7.1.2" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.3" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.1" />
<PackageReference Include="Uno.Microsoft.Xaml.Behaviors.Interactivity.WinUI" Version="2.3.0" />
<PackageReference Include="Uno.Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.3.0" />
Still trying to find minimal sample exhibiting the mentioned issues.
Also tried to check the XAML generated code, but it is too much... well... generated :-(
EDIT:
"Simplified" the use case to following:
Have TabView on a page.
First tab contains ListView with items.
Other tabs contain "opened" items (dummy record).
Item opens on double-click (DoubleTapped) in ListView on first page.
Item closes on X click on a tab (in TabView).
Both TabView and ListView use DataTemplates.
Separate UI from code as much as possible (binding, commands, VMs, etc.).
Based on default WinUI "Hello World" app template.
Project files
EDIT 2:
For anyone interested, based on Andrew KeepCoding's hints and linked issue answers, I mixed code behind with custom attaching of the behaviors to the templated items via new attached property:
public static class InteractionEx
{
public static readonly DependencyProperty AttachBehaviorsProperty = DependencyProperty.RegisterAttached
(
"AttachBehaviors",
typeof(object),
typeof(FrameworkElement),
new PropertyMetadata(false, AttachBehaviorsChanged)
);
public static object GetAttachBehaviors(DependencyObject o) => o.GetValue(AttachBehaviorsProperty);
public static void SetAttachBehaviors(DependencyObject o, object value) => o.SetValue(AttachBehaviorsProperty, value);
private static void AttachBehaviorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behaviors = e.NewValue switch
{
Behavior single => new BehaviorCollection { single },
BehaviorCollection multiple => multiple,
_ => null,
};
Interaction.SetBehaviors(d, behaviors);
}
}
In XAML the behaviors then can be attached to a control like:
<ListView ItemsSource="{x:Bind Items}" ItemTemplateSelector="{StaticResource ListItemTemplateSelector}" SelectedItem="{x:Bind SelectedItem, Mode=TwoWay}">
<local:InteractionEx.AttachBehaviors>
<interactivity:BehaviorCollection>
<core:EventTriggerBehavior EventName="DoubleTapped">
<core:InvokeCommandAction Command="{x:Bind OpenCommand}" />
<core:CallMethodAction MethodName="Open" TargetObject="{x:Bind}" />
</core:EventTriggerBehavior>
</interactivity:BehaviorCollection>
</local:InteractionEx.AttachBehaviors>
</ListView>
I think you'll find these answers helpful.
But let me suggest another option. I have spent some time with your repro project and in my opinion, I think it'd be cleaner and readable if you drop Interaction.Behaviors. Code-behind is not evil if it's UI related and no business logic there.
MainWindow.xaml
<Window
x:Class="TabViewTest.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:helpers="using:TabViewTest.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="using:TabViewTest.ViewModels"
mc:Ignorable="d">
<Grid>
<Grid.Resources>
<DataTemplate x:Key="BrowserTemplate" x:DataType="viewmodels:BrowserTabViewModel">
<TabViewItem Header="Browser">
<ListView x:Name="ItemList" ItemsSource="{x:Bind Items}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:ItemViewModel">
<TextBlock DoubleTapped="TextBlock_DoubleTapped" Text="{x:Bind Id}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</TabViewItem>
</DataTemplate>
<DataTemplate x:Key="ContentTemplate" x:DataType="viewmodels:ContentTabViewModel">
<TabViewItem Header="{x:Bind ItemViewModel.Id}">
<TextBlock Text="{x:Bind ItemViewModel.Id}" />
</TabViewItem>
</DataTemplate>
<helpers:TabItemTemplateSelector
x:Key="TabTemplateSelector"
BrowserTemplate="{StaticResource BrowserTemplate}"
ContentTemplate="{StaticResource ContentTemplate}" />
</Grid.Resources>
<TabView
TabCloseRequested="TabView_TabCloseRequested"
TabItemTemplateSelector="{StaticResource TabTemplateSelector}"
TabItemsSource="{x:Bind ViewModel.Tabs}" />
</Grid>
</Window>
MainWindow.xaml.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Linq;
namespace TabViewTest.ViewModels;
[ObservableObject]
public partial class MainWindowViewModel
{
[ObservableProperty]
private ObservableCollection<TabViewModel> tabs = new();
private readonly BrowserTabViewModel browserTabViewModel;
public MainWindowViewModel()
{
browserTabViewModel = new();
Tabs.Add(browserTabViewModel);
}
[RelayCommand]
private void NewTabRequest(ItemViewModel itemViewModel)
{
Tabs.Add(new ContentTabViewModel(itemViewModel));
}
[RelayCommand]
private void CloseTabRequest(ItemViewModel itemViewModel)
{
if (Tabs
.OfType<ContentTabViewModel>()
.Where(x => x.ItemViewModel == itemViewModel)
.FirstOrDefault() is ContentTabViewModel target)
{
Tabs.Remove(target);
}
}
}
I wanted to create a project based on the MVVM pattern.
Unfortunately, I didn't quite get it right and now I have numerous subsequent errors.
MainWindow.xaml
<Window x:Class="MyProject.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:local="clr-namespace:MyProject"
xmlns:viewmodels="clr-namespace:MyProject.ViewModels"
xmlns:views="clr-namespace:MyProject.View"
mc:Ignorable="d"
Height="571.4" Width="730">
<Window.Resources>
<DataTemplate x:Name="Window1ViewTemplate" DataType="{x:Type viewmodels:Window1ViewModel}">
<views:Window1View DataContext="{Binding}"/>
</DataTemplate>
</Window.Resources>
<Grid>
// Grid Definition not included - not relevant
<DockPanel Background="Blue" Grid.Row="1" Grid.Column="0" Grid.RowSpan="3">
<StackPanel>
<Button Content="Window1"
Height="45"
Click="Window1_click"/>
</StackPanel>
</DockPanel>
<ContentControl Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3" Grid.RowSpan="4" Content="{Binding}"/>
</Grid>
</Window>
MainWindow.xaml.cs
namespace MyProject
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window1_click(object sender, RoutedEventArgs e)
{
DataContext = new Window1ViewModel();
}
}
}
I have a window which I named Window1. This one is not quite relevant I guess.
Now the Click Event button opens the window correctly. But this does not fit the MVVM pattern.
This will start a new thread, which then leads to countless subsequent errors.
How can I adjust the button event so that the binding is correct again.
Some suggestions for you.
Create a MainViewModel class, and set the DataContext of MainWindow to an instance of this.
If possible, don't use event handlers in MVVM, especially not for button click events. Instead, in MainViewModel, create a property of type ICommand, and bind the button's Command property to it.
Depending on what (library) you're using to implement the MVVM pattern, you may already have an ICommand implementation, such as MVVMLight's RelayCommand. If not, you can use a basic ICommand implementation such as
public class BasicCommand: ICommand
{
private readonly Action _execute;
public Command(Action execute)
{
_execute = execute;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_execute?.Invoke();
}
public event EventHandler CanExecuteChanged;
}
In MainViewModel, create a property of type object ContentViewModel, and bind the ContenControl's Content property to it. This property should implement INotifyPropertyChanged as you're going to update it from within the ViewModel.
In the corresponding method for the command, set the ContentViewModel property to an appropriate value - e.g. a Window1ViewModel instance.
In the ContentControl resources, define a DataTemplate for each type you might want to display. This needs to be based on a something other than a Window - eg Grid or CustomControl or UserControl.
.
<ContentControl
Margin="8"
Content="{Binding SelectedItem}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type vm:Window1ViewModel}">
<ctrl:Window1Display DataContext="{Binding}" />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:Window2ViewModel}">
<ctrl:Window2Display DataContext="{Binding}" />
</DataTemplate>
...
</ContentControl.Resources>
</ContentControl>
More details, and a working example on my blog post.
I have the following xaml view:
<UserControl x:Class="MyViews.PersonView"
xmlns:views="clr-namespace:MyViews"
[...]
>
[...]
<dxb:BarManager x:Name="MainBarManager">
<dxb:BarManager.Items>
<dxb:BarButtonItem x:Name="bbiPrint"
Content="{Binding Print, Source={StaticResource CommonResources}}"
Command="{Binding PrintPersonsCommand}"
CommandParameter="{Binding PersonsCardView, ElementName=CardUserControl}"
/>
</dxb:BarManager.Items>
<Grid>
<Grid.RowDefinitions>
[...]
</Grid.RowDefinitions>
<views:CardView x:Name="CardUserControl" Grid.Row="2"/>
</Grid>
[...]
</UserControl>
The CardView is defined as follows:
<UserControl x:Class="MyViews.CardView"
[...]>
[...]
<dxg:GridControl ItemsSource="{Binding Persons}" SelectedItems="{Binding SelectedPersons}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" SelectionMode="MultipleRow">
[...]
<dxg:GridControl.View>
<dxg:CardView x:Name="PersonsCardView"
[...]
CardTemplate="{StaticResource DisplayCardTemplate}"
PrintCardViewItemTemplate="{StaticResource PrintCardTemplate}"/>
</dxg:GridControl.View>
[...]
</dxg:GridControl>
</UserControl>
The PrintPersonsCommand is defined as follows in my ViewModel:
public class PersonViewModel
{
public PersonViewModel(...)
{
[...]
PrintPersonsCommand = new Prism.Commands.DelegateCommand<DataViewBase>(PrintPersons, CanPrintPersons);
}
public Prism.Commands.DelegateCommand<DataViewBase> PrintPersonsCommand { get; private set; }
private void PrintPersons(DataViewBase view)
{
_printService.ShowGridViewPrintPreview(view);
}
private bool CanPrintPersons(DataViewBase view)
{
return true;
}
}
Now, when I click the Print button, the above PrintPersons method is always fed with null. How do I pass CardUserControl.PersonsCardView in my MyViews.PersonView xaml above, how do I pass that PersonCardView to my command? In other words, how do I fix
CommandParameter="{Binding PersonsCardView, ElementName=CardUserControl}"
to make it work?
Currently, the only solution I've found to this problem is to replace the Command and CommandParameter with
ItemClick="OnPrintBtnClick"
and then in the PersonView's code-behind file to do:
private void OnPrintBtnClick(object sender, ItemClickEventArgs e)
{
var ctxt = DataContext as PersonViewModel;
ctxt.PrintPersonsCommand.Execute(CardUserControl.PersonsCardView);
}
That works but I can't believe there is no other way. I'm not happy with that solution because I don't have the benefits of using the Command any more, like e.g. the automatic evaluation of the Command's CanExecute method. I could also put the CardView's xaml code in the PersonView.xaml but I like my controls to be in separate files because I have the feeling it's more structured and each user control has its own responsibilities which can nicely be split into separate files. Also, that solution binds my view to my view model too tightly.
Can someone help me out please?
Without changing your existing view and viewmodel hierarchy, I was able to pass the GridControl.View to the PersonViewModel using the Tag property
You can assign the CardView to the Tag property at the bottom of your CardView UserControl, and then access this Tag as CommandParameter.
CardView UserControl
<UserControl x:Class="MyViews.CardView"
[...]>
[...]
<dxg:GridControl ItemsSource="{Binding Persons}" SelectedItems="{Binding SelectedPersons}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" SelectionMode="MultipleRow">
[...]
<dxg:GridControl.View>
<dxg:CardView x:Name="PersonsCardView"
[...]
CardTemplate="{StaticResource DisplayCardTemplate}"
PrintCardViewItemTemplate="{StaticResource PrintCardTemplate}"/>
</dxg:GridControl.View>
[...]
</dxg:GridControl>
<UserControl.Tag>
<Binding ElementName="PersonsCardView"/>
</UserControl.Tag>
</UserControl>
Print Button Xaml:
<dxb:BarButtonItem x:Name="bbiPrint"
Content="{Binding Print, Source={StaticResource CommonResources}}"
Command="{Binding PrintPersonsCommand}"
CommandParameter="{Binding ElementName=CardUserControl, Path=Tag}"
/>
Based on the valuable input of Insane, I came up with the following two cleaner fixes:
Code-behind solution
In the PersonView, use the ItemClick event handler on the Print button:
<dxb:BarButtonItem x:Name="bbiPrint"
Content="{Binding Print, Source={StaticResource CommonResources}}"
ItemClick="OnPrintBtnClick"/>
Adapt the corresponding code-behind file like this:
public partial class PersonView : UserControl
{
readonly IPrintService _printService;
public PersonView(IPrintService printService)
{
_printService = printService;
InitializeComponent();
}
private void OnPrintBtnClick(object sender, ItemClickEventArgs e)
{
_printService.ShowGridViewPrintPreview(CardUserControl.PersonsCardView);
}
}
Because I want to gray-out the Print button when there is no selection, I still need to add some code to make that happen. I can get it by
1. updating the button code to
<dxb:BarButtonItem x:Name="bbiPrint"
Content="{Binding Print, Source={StaticResource CommonResources}}"
ItemClick="OnPrintBtnClick" IsEnabled="{Binding CanPrintPersons}"/>
refreshing the CanPrintPersons property in the PersonViewModel upon Persons selection change
That's it.
CardViewModel solution
In that solution, we have a PersonView with its underlying PersonViewModel and a CardView with its underlying CardViewModel. I will not describe that solution with all the details as it is overkill in my situation but for the sake of completeness, I'll give the main points. Upon clicking the Print button on the PersonView, the PersonViewModel's PrintCommand is called. That command emits a Print event to the CardViewModel which in turn calls its own PrintCommand. That latter command calls
_printService.ShowGridViewPrintPreview(View);
where the View is a CardViewModel's property that is set upon CardView loading with e.g.
<dxmvvm:Interaction.Behaviors>
<dxmvvm:EventToCommand EventName="Loaded" Command="{Binding ViewLoadedCommand}" CommandParameter="{Binding ElementName=PersonsCardView}" />
</dxmvvm:Interaction.Behaviors>
Because I have two child views I want to print, I'd need to add a view model for each one of those. In addition, those two view models plus the PersonViewModel need access to the list of Persons to be printed. In particular, they need a shared access to the same data, so that they are synchronized. A simple way to do that is explained here and is totally doable. But I think it is not worth the trouble for the simple use case I have as it adds more complexity than necessary.
I want to bind an element in a page to dependency property in code behind with compiled binding and same time bind another element to ViewModel with usual binding. But it gives a runtime error.
Here is my xaml code.
<Page
x:Class="XbindingProblem.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:XbindingProblem"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding Main, Source={StaticResource Locator}}"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="UserDataTemplate" x:DataType="local:User">
<StackPanel>
<TextBlock Text="{x:Bind Name}" />
<TextBlock Text="{x:Bind Age}" />
</StackPanel>
</DataTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<TextBlock Text="{Binding Title}"/>
<ContentPresenter ContentTemplate="{StaticResource UserDataTemplate}" Content="{x:Bind CurrentUser, Mode=OneWay}"/>
</StackPanel>
</Grid>
Here CurrentUser is dependency property which is initially null and then change in run time. This gives following runtime error.
Incorrect type passed into template. Based on the x:DataType global::XbindingProblem.User was expected.
The problem is it passes the ViewModel into UserDataTemplate instead of CurrentUser dependency property when CurrentUser is null.
Can anyone have a good explanation on this problem?
If you remove DataContext="{Binding Main, Source={StaticResource Locator}}", it will work. Why? because {x:Bind CurrentUser} is looking for a property called CurrentUser sitting inside your MainPage.xaml.cs. Since the CurrentUser is indeed a dependency property of your page, it will just work.
However, by specifying the DataContext of your page, the x:Bind is now excepting a CurrentUser property inside your MainViewModel instance, and of course it's not going to find it, so a compile-time error will be thrown.
One possible fix is to set the this.CurrentUser really early, even before calling InitializeComponent.
this.CurrentUser = new User();
InitializeComponent();
But this is IMHO not the the right way of doing things, as it's basically a racing game - it tries to populate the ContentPresenter before the DataContext gets updated, and in the end you will end up having the TextBlock (of which Text binds to Title) and the ContentPresenter attached to different contexts!
So ask yourself why you need to create a dependency property for CurrentUser inside a Page object, instead of having a normal property (with INotifyPropertyChanged implementation) sitting inside your MainViewModel? I'd prefer the latter, 'cause it's more semantically correct.
The question is interesting, what I have done is just remove the datacontext and this is the code behind is similar to yours:
public sealed partial class BlankPage1 : Page
{
public User CurrentUser
{
get { return (User)GetValue(CurrentUserProperty); }
set { SetValue(CurrentUserProperty, value); }
}
// Using a DependencyProperty as the backing store for CurrentUser. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentUserProperty =
DependencyProperty.Register("CurrentUser", typeof(User), typeof(BlankPage1), new PropertyMetadata(null));
public BlankPage1()
{
this.InitializeComponent();
CurrentUser = new User() { Name = "Hello", Age = "20" };
}
}
public class User
{
public String Name { get; set; }
public String Age { get; set; }
}
might be you have the User class in another namespace or have another class in the typeof(...) of the dependency property. Because I tested that and works. The DataContext of the page can be whatever you want it won't affect.
Then I added the datacontext just to test:
<Page.DataContext>
<local:Main/>
</Page.DataContext>
and the code just for testing:
public class Main
{
public String Title { get; } = "title";
public User MainUser { get; set; }
}
And it does not throws any exception, appears the Main data and the CurrentUser data.
UPDATE. The error happens when the User is null so it is like the x:Bind is null it propagates to the Binding,To solve that (it was tough):
<Page x:Name="Root"
x:Class="Deletetb.BlankPage1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Deletetb"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" >
<Page.DataContext>
<local:Main/>
</Page.DataContext>
<Page.Resources>
<DataTemplate x:Key="UserDataTemplate" x:DataType="local:User">
<StackPanel>
<TextBlock Text="{x:Bind Name}" />
<TextBlock Text="{x:Bind Age}" />
</StackPanel>
</DataTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel DataContext="{x:Null}">
<TextBlock Text="{Binding DataContext.Title, ElementName=Root}" />
<ContentPresenter ContentTemplate="{StaticResource UserDataTemplate}" Content="{x:Bind CurrentUser, Mode=OneWay}"/>
</StackPanel>
</Grid>
</Page>
Where is binding defined (TextBlock) I set the datacontext to null in the parent container (StackPanel) and bind by element name, and it does not crash, also I added a wait by code to test and set the Current User and it works. That was a challenge. hope it also works for you.
Although it breaks the idea of MVVM a bit, you can add a property to your page like this:
public MainViewModel viewModel => DataContext as MainViewModel;
And then in the XAML code reference the page property
<ContentPresenter Content="{x:Bind viewModel.CurrentUser, Mode=OneWay}" />
I am using MVVM for my application and have a form that allows the user to enter basic personnel information. The form includes a UserControl which is, basically, an ItemsControl that includes textBoxes that can be dynamically created. This is a simplified version:
<ItemsControl x:Name="items" ItemsSource="{Binding MyItemsCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="row">
<TextBox x:Name="textBox" Text="{Binding ContactInfo, ValidatesOnExceptions=True}" extensions:FocusExtension.IsFocused="{Binding IsFocused}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button x:Name="NewItemButton" Command="{Binding AddItemToMyCollectionCommand}" />
I want the TextBox that has just been created to receive focus, therefore I added an attached property. This is part of it:
public static readonly DependencyProperty IsFocusedProperty =
DependencyProperty.RegisterAttached("IsFocused", typeof(bool), typeof(FocusExtension), new UIPropertyMetadata(false, OnIsFocusedPropertyChanged));
private static void OnIsFocusedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uie = (UIElement)d;
if ((bool)e.NewValue)
{
uie.Focus();
}
}
In the form that contains the UserControl there are several other text boxes before and after. The UserControl has its own ViewModel, which I set as the DataContext of the control through a property in the container's ViewModel. Basically, a simplified version of the container looks like this:
<StackPanel Orientation="Horizontal" />
<TextBox x:Name="firstName" />
<TextBox x:Name="lastName" />
<local:DynamicFormUserControl
x:Name="phones"
DataContext="{Binding PhonesViewModel}" />
<local:DynamicFormUserControl
x:Name="emails"
DataContext="{Binding EmailsViewModel}" />
<TextBox x:Name="address" />
</StackPanel>
My problem is that I want the firstName TextBox to get the focus when the form is loaded for the first time, but the form keeps on placing the focus on the first TextBox of the phones UserControl. I tried to override it by using firstName.Focus() on the Loaded event of the form, but this didn't work, and no matter what I tried the focus is still on the phones userControl instead of the first element in the form that contains it.
Does anybody have any idea how to solve this?
Thanks.
Here you go
add FocusManager.FocusedElement="{Binding ElementName=firstName}" to your stack panel
<StackPanel Orientation="Horizontal"
FocusManager.FocusedElement="{Binding ElementName=firstName}"/>
<TextBox x:Name="firstName" />
<TextBox x:Name="lastName" />
<local:DynamicFormUserControl
x:Name="phones"
DataContext="{Binding PhonesViewModel}" />
<local:DynamicFormUserControl
x:Name="emails"
DataContext="{Binding EmailsViewModel}" />
<TextBox x:Name="address" />
</StackPanel>
also notice that you may need to prevent items control in the user control from focusing itself
<ItemsControl x:Name="items" Focusable="False" >
<ItemsControl.ItemTemplate>
I guess I managed to find a solution. The problem was that the form I created was itself a user control inside a window, and never got focus. I didn't think that would be relevant so I didn't mention it in my previous post- sorry. I found in this solution for forcing focus to a user control.
Basically, when I have a UserControl inside a window it doesn't get focus even if I try to set the focus with either Focus() or FocusedElement. So to overcome this problem I found on a different post a workaround. Basically I added it to the code-behind of the UserControl that contains the firstName TextBox. If we call the UserControl, say, PersonalInfoUserControl, the constructor of the control would look like this:
public PersonalInfoUserControl()
{
InitializeComponent();
this.IsVisibleChanged += new DependencyPropertyChangedEventHandler(UserControl_IsVisibleChanged);
}
I added an event handler to the IsVisibleChanged event of the control. The method would look like this:
void UserControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue == true)
{
Dispatcher.BeginInvoke(
DispatcherPriority.ContextIdle,
new Action(delegate()
{
firstName.Focus();
}));
}
}