WPF - Problem with Prism regions and relative source to ancestor binding - c#

I tried to create a simple example of my problem:
Lets say we have the following UserControl with a Label, Image, region and a Button:
<UserControl x:Class="SR.Soykaf.Client.Main.Simple.SimpleTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:core="clr-namespace:SR.Soykaf.Client.Core.Core;assembly=SR.Soykaf.Client.Core"
xmlns:regions="http://prismlibrary.com/"
mc:Ignorable="d"
regions:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Center" Content="TITLE" />
<Image Grid.Row="1" Stretch="Uniform" UseLayoutRounding="True" Height="200" Width="153" Source="{Binding PortraitImage}" />
<ContentControl Grid.Row="2" regions:RegionManager.RegionName="{x:Static core:RegionNames.CombatWizardRegion}" />
<Button Grid.Row="3" Content="Change view in region" Command="{Binding ChangeViewCommand}" />
</Grid>
</Grid>
</UserControl>
With this ViewModel:
public class SimpleTabViewModel : BaseViewModel
{
private int _state;
private BitmapImage _portraitImage;
public BitmapImage PortraitImage
{
get => _portraitImage;
set => SetProperty(ref _portraitImage, value);
}
public ICommand ChangeViewCommand { get; set; }
public SimpleTabViewModel()
{
SetPortraitImage();
_state = 0;
ChangeViewCommand = new DelegateCommand(ChangeView);
}
private void ChangeView()
{
if (_state == 0)
{
RegionManager.RequestNavigate(RegionNames.CombatWizardRegion, new Uri(nameof(WeaponWizardView), UriKind.Relative));
_state = 1;
}
else
{
RegionManager.RequestNavigate(RegionNames.CombatWizardRegion, new Uri(nameof(SpellWizardView), UriKind.Relative));
_state = 0;
}
}
private void SetPortraitImage()
{
PortraitImage = new BitmapImage();
var resource = typeof(Data.Characters.PlayerCharacter).Assembly.GetManifestResourceNames().Single(x => x.ContainsIgnoreCase("PC_Hawk"));
using (var stream = typeof(Data.Characters.PlayerCharacter).Assembly.GetManifestResourceStream(resource))
{
PortraitImage.BeginInit();
PortraitImage.StreamSource = stream;
PortraitImage.CacheOption = BitmapCacheOption.OnLoad;
PortraitImage.EndInit();
}
}
}
As you can see, the Button will switch between 2 views for the CombatWizardRegion region. These are the simple views:
View 1:
<UserControl x:Class="SR.Soykaf.Client.Main.Simple.SpellWizardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mvvm="http://prismlibrary.com/"
mc:Ignorable="d"
mvvm:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Center" Content="Spell" />
<Image Grid.Row="1" Stretch="Uniform" UseLayoutRounding="True" Height="200" Width="153" Source="{Binding DataContext.PortraitImage, RelativeSource={RelativeSource AncestorType={x:Type UserControl}, AncestorLevel=2}}" />
</Grid>
</UserControl>
View 2:
<UserControl x:Class="SR.Soykaf.Client.Main.Simple.WeaponWizardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mvvm="http://prismlibrary.com/"
mc:Ignorable="d"
mvvm:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" HorizontalAlignment="Center" Content="Weapon" />
<Image Grid.Row="1" Stretch="Uniform" UseLayoutRounding="True" Height="200" Width="153" Source="{Binding DataContext.PortraitImage, RelativeSource={RelativeSource AncestorType={x:Type UserControl}, AncestorLevel=2}}" />
</Grid>
</UserControl>
These two views basically bind their image source to the PortraitImage property of the parent view model. Both views also have a unique label: Spell and Weapon to distinguish between the two of them.
Then I register the 2 views:
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
Launching this application works fine - on first sight:
But... then we click the button to switch views:
Why is the image not loading in the second view? It has the same binding code as the first view.
(Also if I first register the WeaponCombatView to the region, then that view works but the SpellCombatView doesn't work anymore.)
I get this error for the view which is registered last:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.UserControl', AncestorLevel='2''. BindingExpression:Path=DataContext.PortraitImage; DataItem=null; target element is 'Image' (Name=''); target property is 'Source' (type 'ImageSource')
Also interesting : During debugging if I change the AncesterLevel to 3 and back to 2, the image suddenly appears because the binding seems to get refreshed. I also checked the Visual Tree and I don't see any problems.
Thanks in advance!

The RegisterViewWithRegion method enables view discovery, but is intended to construct and show the specified view when the control is loaded. The navigation service allows for changing the view dynamically. Typically, you would use RegisterViewWithRegion for static views like a menu, that do not change.
[..] if I first register the WeaponCombatView to the region, then that view works but the SpellCombatView doesn't work anymore.
Your CombatWizardRegion is in a ContentControl which uses a SingleActiveRegion in the default Prism region adapter. This means, that it can only display a single active view at once in it. When using RegisterViewWithRegion to register multiple views, they will be registered and both will be added to the Views collection of the region, but only the first one will be added to the ActiveViews collection and displayed. For your registrations below, effectively only the first one will be displayed.
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
After navigation your views do not display, because you did not register them for navigation in the container, so the navigation service will not find them. The region manager will store them different internally with RegisterViewWithRegion, that is why you cannot just register them that way. Instead register them like this in Prism >=7:
containerRegistry.RegisterForNavigation<SpellWizardView>();
containerRegistry.RegisterForNavigation<WeaponWizardView>();
For older versions of Prism <=6 you have to use one of these methods:
containerRegistry.RegisterTypeForNavigation<SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register<object, SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register(typeof(object), typeof(SpellWizardView), nameof(SpellWizardView));
I recommend you to use either RegisterViewWithRegion for static regions or the navigation service for dynamic regions, where you need to change views with the navigation service. You can navigate to the initial view instead of registering it with the region manager and RegisterViewWithRegion.
I get this error for the view which is registered last: System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.UserControl', AncestorLevel='2''.
This binding works fine, it is failing because of the single active region issue above. Both views get added to the Views collection of the CombatWizardRegion, but only the first is added to the ActiveViews collection and displayed and therefore set as Content of the corresponding ContentControl. Consequently, the first view is in the visual tree and receives the data context, while the second view is not in the visual tree, so its data context is null and there is no ancestor, which causes the error.

Related

WinUI 3 ItemsRepeater re-orders/removes items when scrolling within a ScrollViewer and using UniformGridLayout

Wanted to post my issue I have with WinUI 3 in .NET 6 here too, maybe it's not a bug and I am doing something wrong.
First of all a link to the issue on Github: https://github.com/microsoft/microsoft-ui-xaml/issues/4516
A sample project can be found here: https://github.com/brechtb86/microsoft-ui-xaml-issues
Describe the bug
When using an ItemsRepeater in a ScrollViewer on a Page within a NavigationView with a custom user control in the ItemTemplate and you start to scroll up and down, the items are re-ordered or even removed. The items are just simple objects and a UserControl bound to some properties of that object.
Steps to reproduce the bug
Open the page "ItemsRepeater Scroll Issue (re-order/remove)". Items are ordered correctly.
Start scrolling up and down. Items are re-ordered and sometimes removed.
Switch to another page.
Switch back to page "ItemsRepeater Scroll Issue (re-order/remove)". Items are ordered correctly again.
Rinse and repeat.
Expected behavior
Items should stay in the same order.
I'm using the MVVM pattern, I'm using DI to manage my viewmodels with Ninject. The project is very simple:
ItemsRepeaterScrollIssuePage.xaml:
<Page
x:Class="WinUI3Issues.ItemsRepeaterScrollIssuePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUI3Issues"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:WinUI3Issues.Models"
xmlns:userControls="using:WinUI3Issues.UserControls"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
DataContext="{Binding Path=ItemsRepeaterScrollIssueViewModel, Source={StaticResource ViewModelLocator}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Start scrolling" />
<ScrollViewer Grid.Row="1">
<ItemsRepeater ItemsSource="{Binding Path=DummyObjects}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" ItemsStretch="Fill" MinColumnSpacing="24" MinRowSpacing="24"></UniformGridLayout>
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="models:DummyObject">
<Grid>
<userControls:DummyObjectControl Title="{Binding Path=Name}"></userControls:DummyObjectControl>
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</Page>
DummyObjectControl:
<UserControl
x:Class="WinUI3Issues.UserControls.DummyObjectControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUI3Issues.UserControls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Border Height="288" Width="192" BorderThickness="1" BorderBrush="{ThemeResource TextBoxBorderThemeBrush}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="16" Text="{x:Bind Path=Title, Mode=OneWay}"></TextBlock>
</Grid>
</Border>
ItemsRepeaterScrollIssueViewModel:
public class ItemsRepeaterScrollIssueViewModel : BaseViewModel
{
public ObservableCollection<DummyObject> DummyObjects { get; set; }
public ItemsRepeaterScrollIssueViewModel()
{
this.DummyObjects = new ObservableCollection<DummyObject>();
for (var i = 1; i <= 100; i++)
{
this.DummyObjects.Add(new DummyObject(){ Name = $"Object_{i}"});
}
}
}
DummyObject.cs:
public class DummyObject
{
public string Name { get; set; }
}
ViewModelLocator.cs:
public class ViewModelLocator
{
public ItemsRepeaterScrollIssueViewModel ItemsRepeaterScrollIssueViewModel => IocKernel.Get<ItemsRepeaterScrollIssueViewModel>();
public XamlCommandBindingIconSourceIssueViewModel XamlCommandBindingIconSourceIssueViewModel => IocKernel.Get<XamlCommandBindingIconSourceIssueViewModel>();
}
Do any of you know if I might be doing something wrong or if this is a legit bug in WinUI 3?
OK, I got help on Github, the issue was that my dependency properties were not marked as static, another issue was that I didn't set the x:Bind mode to OneWay in my UserControl.

Caliburn.Micro - Binding a button in a sidebar to a method in a ViewModel

I have a problem with binding a button located in a sidebar in my windows phone app. It seems like the buttons binding just dissapears..
Here's my code at the moment
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<sidebar:SidebarControl x:Name="sidebarControl"
HeaderText="WP"
HeaderBackground="YellowGreen"
HeaderForeground="White"
SidebarBackground="{StaticResource PhoneChromeBrush}">
<sidebar:SidebarControl.SidebarContent>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="380">
<Button Content="Go to page 2" x:Name="GoToPage2"/>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
</Grid>
</sidebar:SidebarControl.SidebarContent>
<Grid VerticalAlignment="Top" HorizontalAlignment="Stretch"
Margin="12">
<TextBlock Style="{StaticResource PhoneTextNormalStyle}">Your current view goes here</TextBlock>
</Grid>
</sidebar:SidebarControl>
</Grid>
</Grid>
At the moment I am using a nuget for the sidebar called SidebarWP8. Maybe Calinbrun.Micro doesnt work with this? Or do I have to insert a binding to the VM in a grid?
Here's the method in the ViewModel:
private readonly INavigationService navigationService;
public MainPageViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
public void GoToPage2()
{
navigationService.UriFor<Page2ViewModel>().Navigate();
}
<Button cm:Message.Attach="[Event Click] = [Action GoToPage2()]" />
This should work, because of the other commenter is correct with respect to the default controls... Custom controls require adding some more handling in which can be a pain in the butt but doing it with the short hand above CM will look for that property in the button and process accordingly.
I checked the source for Caliburn Micro: I handles the convention binding by traversing the visual tree to find elements with a name. But it only checks the default Content property for each control.
That means it will only go through the Content or Children elements and not Custom properties like SidebarContent, so named elements will not be found there.
You have to wire it up by hand (by binding a command or adding a click handler).

DataItem=null on binding, can't find out why?

I am trying to reproduce what is suggested in Sheridan's answer to this question to navigate trough my views when using WPF with the MVVM pattern. Unfortunately, I am getting a binding error when I do so. Here is the exact error:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='JollyFinance.ViewModels.MainViewModel', AncestorLevel='1''. BindingExpression:Path=DataContext.DisplayTest; DataItem=null; target element is 'Button' (Name=''); target property is 'Command' (type 'ICommand')
When I look into my xaml code in LoginView.xaml, I noticed that Visual Studio is telling me that it cannot find DataContext.DisplayText in context of type MainViewModel. I have tried removing DataContext. and just keeping DisplayText instead, but to no avail.
Unless Sheridan's answer has an error, I am most definitely missing something here. What should I do for it to work?
MainWindow.xaml:
<Window x:Class="JollyFinance.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:JollyFinance.ViewModels"
xmlns:views="clr-namespace:JollyFinance.Views"
Title="JollyFinance!" Height="720" Width="1280">
<Window.Resources>
<!-- Different pages -->
<DataTemplate DataType="{x:Type vm:LoginViewModel}">
<views:LoginView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:TestViewModel}">
<views:Test/>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding CurrentViewModel}"/>
</Grid>
</Window>
MainViewModel.cs:
public class MainViewModel : BindableObject
{
private ViewModelNavigationBase _currentViewModel;
public MainViewModel()
{
CurrentViewModel = new LoginViewModel();
}
public ICommand DisplayTest
{
get
{
// This is added just to see if the ICommand is actually called when I press the
// Create New User button
Window popup = new Window();
popup.ShowDialog();
// View model that doesn't contain anything for now
return new RelayCommand(action => CurrentViewModel = new TestViewModel());
}
}
public ViewModelNavigationBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
if (_currentViewModel != value)
{
_currentViewModel = value;
RaisePropertyChanged("CurrentViewModel");
}
}
}
}
LoginView.xaml:
<UserControl x:Class="JollyFinance.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:JollyFinance.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<vm:LoginViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Username: " Grid.Column="1" Grid.Row="1" Margin="5"/>
<TextBox Text="{Binding Path=Username}" Grid.Column="2" Grid.Row="1" Grid.ColumnSpan="2" Margin="5"/>
<TextBlock Text="Password: " Grid.Column="1" Grid.Row="2" Margin="5"/>
<PasswordBox x:Name="PasswordBox" PasswordChar="*" Grid.Column="2" Grid.ColumnSpan="2" Grid.Row="2" Margin="5"/>
<Button Content="Log In" Grid.Column="2" Grid.Row="3" Margin="5" Padding="5" Command="{Binding LoginCommand}"/>
<Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5"
Command="{Binding DataContext.DisplayTest, RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}},
Mode=OneWay}"/>
</Grid>
</UserControl>
LoginViewModel.cs:
public class LoginViewModel : ViewModelNavigationBase
{
public LoginViewModel()
{
LoginCommand = new RelayCommand(Login);
}
private void Login(object param)
{
// Just there to make sure the ICommand is actually called when I press the
// Login button
Window popup = new Window();
popup.ShowDialog();
}
public String Username { get; set; }
public String Password { get; set; }
public ICommand LoginCommand { get; set; }
}
ViewModelNavigationBase is just a class that implements the INotifyPropertyChanged interface, and Test.xaml and TestViewModel.cs are just a dummy viewmodel/view for test purposes.
In my answer, I stated that you should declare your view model DataTemplates in App.xaml so that every view will have access to them. Putting them in the MainWindow class is your first problem.
Another mistake is your Binding Path for your ICommand. If you want to access something from the view model that is set as the Window.DataContext, then you should not use a RelativeSource Binding . Try this instead:
<Button Content="Create new user" Grid.Column="3" Grid.Row="3" Margin="5" Padding="5"
Command="{Binding DataContext.DisplayTest}, Mode=OneWay}" />
Also remember that for whatever reason, you chose not to make your MainViewModel class extend the ViewModelNavigationBase class... that could also cause you problems.
Anyway, if that doesn't sort out your problems, just let me know. Also, if you want to notify a user at anytime on Stack Overflow, just put an # symbol in front of their name and they will receive a notification. You could have asked me this question directly if you had done that.
MainViewModel is not a direct ancestor in the visual or logical tree, which is why RelativeSource={RelativeSource AncestorType={x:Type vm:MainViewModel}} cannot find it.
How do you fix it? First, please don't try and reach through various UI components like this to trigger commands. Just because you saw it somewhere on the internet doesn't mean it is a desirable design choice. Doing this means the LoginView has a deep understanding of other views and view models - which is bad. If you are going to do that then you might as well code everything as one single UI class with a single viewmodel that is really just a massive code behind class.
A better (but still not optimal) approach is to have the MainView (or viewmodel) spawn the LoginView. As it holds the reference to the view, it is also responsible for disposing of it. So the LoginView can be shown to collect credentials, then the main view can dispose if it signals that the credentials are validated successfully. Or it can just collect credentials and leave it up to the MainView/viewmodel to validate them (which can be done by the MainView/viewmodel triggering a background call to check the credentials against a store).
A simple (crude) rule of thumb is: a parent view can know about a child view, but in general the reverse should not happen. MVVM is about decoupling and segregating functionality, but instead you are tightly coupling them. Of course all this gets a whole lot more complex than what I've illustrated, but you can still do some of this while keeping it practical and not over-engineering.
So, TLDR;:
the LoginView (or its viewmodel) should implement its own command to deal with the button click
don't reach deep through the entrails of another view to trigger functionality
strive for SRP and de-coupled code/views
when using ancestor binding, look for something that's in the visual/logical tree
Define MainViewModel in App scope as a static resource.
<App.Resources>
<MainViewModel x:Key="MainViewModel" />
</App.Resources>
Then you will be able to bind MainViewModel commands from any view.
<Button Command="{Binding Source={StaticResource MainViewModel}, Path=DisplayTest}" />
EDIT
Or try this code:
<Button Command="{Binding DisplayTest, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window), Path=DataContext}}"/>

New item activated in Conductor does not show it

I'm activating new item in conductor using Caliburn.Micro.Contrib's ConductResult. Conductor is of type Conductor<IScreen>.Collection.OneActive, and there is already one item showing and working correctly.
The new item however is not shown after it was activated. I already checked and the conductor's ActiveItem is set to that new item, new item is activated as well. View's IsVisible of new item is also set to true, so I don't understand why it is not visible.
XAML of the conductor's view is pretty simple:
<UserControl x:Class="..."
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cal="http://www.caliburnproject.org"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=ActiveItem, Converter={StaticResource objectTypeConverter}}" Margin="5" />
<ItemsControl Grid.Row="1" ItemsSource="{Binding Items}" BorderBrush="Aqua" BorderThickness="10 ">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Converter={StaticResource objectTypeConverter},ConverterParameter=something}" Margin="5" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ContentControl Grid.Row="2" x:Name="ActiveItem" />
</Grid>
</UserControl>
(TextBlock and ItemsControl are there for debugging purposes; they prove that new item is conducted within conductor (i.e. Items collection contains it) and new item is set as ActiveItem)
In my case, I was using IoC.Get<IShell> to access the parent viewmodel.
The default bootstrapper says container.PerRequest<IShell, ShellViewModel>();
That’s why I was getting another instance of my ShellViewModel class, activated the item just fine, but no UI was updating.
One way to fix is replace container.PerRequest with container.Singleton.
Another is change IoC.Get<IShell>() into ( (IShell)Parent )

Replace contents WPF grid control by grid in other XAML file

I'm trying to replace the content of a WPF grid control by another WPF grid defined in a second XAML file in code (c#).
(simplified example)
Window1.xaml:
<Window x:Class="Demo1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_Set Grid" Click="MenuItem_Click" />
</Menu>
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem Name="statusItem">Status</StatusBarItem>
</StatusBar>
<Grid Name="header" DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Name="txtHi" Grid.Row="0" Grid.Column="0">Hi</TextBlock>
<TextBlock Name="txtName" Grid.Row="0" Grid.Column="1">X</TextBlock>
</Grid>
<Grid Name="gridContent">
</Grid>
</DockPanel>
Windows2.xaml contains the grid that replaces gridContent
<Window x:Class="Demo1.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<Grid Name="grid2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Grid.Column="1" Grid.Row="1">Hello !!!</Label>
</Grid>
The MenuItem_Click event in the code behind Windows1.xaml.cs contains:
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
Window2 win2 = new Window2();
gridContent = win2.grid2;
setStatus();
}
private void setStatus() {
statusItem.Content = "gridContent has " + gridContent.RowDefinitions.Count + " rows and " + gridContent.ColumnDefinitions.Count + " columns.";
}
Although the statusItem say the gridContent contains 2 rows and 2 columns after a click on the menu, the window is not changed and does not contain the text Hello!!!
Any ideas what I'm doing wrong?
If there are better solutions to "embed" a grid from a second xaml file, please let me know.
Thanks,
Robbie
Replacing the value of the gridContent variable cannot have an effect on the controls tree.
You must first disconnect the grid2 from its parent and then add it to the children of gridContent, like this:
win2.Content = null;
gridContent.Children.Add(win2.grid2);
This works (I tried), but it is not the recommended way to create a Window, extract its content then place it in another window. You should use a UserControl in place of Window2 and then you can put it directly inside gridContent.

Categories