WPF - Prism 区域问题和祖先绑定的相关来源
WPF - Problem with Prism regions and relative source to ancestor binding
我试图创建一个简单的问题示例:
假设我们有以下 UserControl
,其中 Label
、Image
、区域和 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>
使用此 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();
}
}
}
如您所见,Button
将在 CombatWizardRegion
区域的 2 个视图之间切换。这些是简单的观点:
视图 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>
视图 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>
这两个视图基本上都是将它们的图片源绑定到父视图模型的PortraitImage
属性上。这两个视图也有一个唯一的标签:Spell
和 Weapon
来区分他们两个。
然后我注册 2 个视图:
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
启动此应用程序工作正常 - 乍一看:
但是...然后我们点击按钮切换视图:
为什么第二个视图中没有加载图像?它与第一个视图具有相同的绑定代码。
(此外,如果我首先将 WeaponCombatView 注册到该区域,则该视图有效但 SpellCombatView 不再有效。)
最后注册的视图出现此错误:
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')
同样有趣:在调试过程中,如果我将 AncesterLevel 更改为 3 再更改回 2,图像会突然出现,因为绑定似乎得到了刷新。我还检查了可视化树,没有发现任何问题。
提前致谢!
RegisterViewWithRegion
方法启用视图发现,但旨在在控件为loaded时构造并显示指定视图。导航服务允许动态更改视图。通常,您可以将 RegisterViewWithRegion
用于 静态视图 ,就像菜单一样,不会更改。
[..] if I first register the WeaponCombatView to the region, then that view works but the SpellCombatView doesn't work anymore.
您的 CombatWizardRegion
在 ContentControl
中,它在默认 Prism 区域适配器中使用 SingleActiveRegion
。这意味着,它一次只能显示一个 单个活动视图 。当使用RegisterViewWithRegion
注册多个视图时,它们会被注册并且都被添加到区域的Views
集合中,但只有第一个会被添加到ActiveViews
集合中并且显示。对于下面的注册,实际上只会显示第一个。
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
导航后您的视图不显示,因为您没有在容器中为导航注册它们,所以导航服务将找不到它们。区域经理将在内部将它们存储为 RegisterViewWithRegion
,这就是为什么您不能以这种方式注册它们的原因。而是像这样在 Prism >=7:
中注册它们
containerRegistry.RegisterForNavigation<SpellWizardView>();
containerRegistry.RegisterForNavigation<WeaponWizardView>();
对于旧版本的 Prism <=6,您必须使用以下方法之一:
containerRegistry.RegisterTypeForNavigation<SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register<object, SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register(typeof(object), typeof(SpellWizardView), nameof(SpellWizardView));
我建议您对静态区域使用 RegisterViewWithRegion
或对动态区域使用导航服务,您需要使用导航服务更改视图。您可以导航到初始视图,而不是向区域管理器和 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''.
此绑定工作正常,由于上述 单个活动区域 问题而失败。两个视图都添加到 CombatWizardRegion
的 Views
集合,但只有第一个被添加到 ActiveViews
集合并显示,因此设置为相应 [=] 的 Content
16=]。因此,第一个视图在可视化树中并接收数据上下文,而第二个视图不在可视化树中,因此它的数据上下文是null
并且有没有祖先,这会导致错误。
我试图创建一个简单的问题示例:
假设我们有以下 UserControl
,其中 Label
、Image
、区域和 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>
使用此 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();
}
}
}
如您所见,Button
将在 CombatWizardRegion
区域的 2 个视图之间切换。这些是简单的观点:
视图 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>
视图 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>
这两个视图基本上都是将它们的图片源绑定到父视图模型的PortraitImage
属性上。这两个视图也有一个唯一的标签:Spell
和 Weapon
来区分他们两个。
然后我注册 2 个视图:
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
启动此应用程序工作正常 - 乍一看:
但是...然后我们点击按钮切换视图:
为什么第二个视图中没有加载图像?它与第一个视图具有相同的绑定代码。
(此外,如果我首先将 WeaponCombatView 注册到该区域,则该视图有效但 SpellCombatView 不再有效。)
最后注册的视图出现此错误:
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')
同样有趣:在调试过程中,如果我将 AncesterLevel 更改为 3 再更改回 2,图像会突然出现,因为绑定似乎得到了刷新。我还检查了可视化树,没有发现任何问题。
提前致谢!
RegisterViewWithRegion
方法启用视图发现,但旨在在控件为loaded时构造并显示指定视图。导航服务允许动态更改视图。通常,您可以将 RegisterViewWithRegion
用于 静态视图 ,就像菜单一样,不会更改。
[..] if I first register the WeaponCombatView to the region, then that view works but the SpellCombatView doesn't work anymore.
您的 CombatWizardRegion
在 ContentControl
中,它在默认 Prism 区域适配器中使用 SingleActiveRegion
。这意味着,它一次只能显示一个 单个活动视图 。当使用RegisterViewWithRegion
注册多个视图时,它们会被注册并且都被添加到区域的Views
集合中,但只有第一个会被添加到ActiveViews
集合中并且显示。对于下面的注册,实际上只会显示第一个。
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(SpellWizardView));
_regionManager.RegisterViewWithRegion(RegionNames.CombatWizardRegion, typeof(WeaponWizardView));
导航后您的视图不显示,因为您没有在容器中为导航注册它们,所以导航服务将找不到它们。区域经理将在内部将它们存储为 RegisterViewWithRegion
,这就是为什么您不能以这种方式注册它们的原因。而是像这样在 Prism >=7:
containerRegistry.RegisterForNavigation<SpellWizardView>();
containerRegistry.RegisterForNavigation<WeaponWizardView>();
对于旧版本的 Prism <=6,您必须使用以下方法之一:
containerRegistry.RegisterTypeForNavigation<SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register<object, SpellWizardView>(nameof(SpellWizardView));
containerRegistry.Register(typeof(object), typeof(SpellWizardView), nameof(SpellWizardView));
我建议您对静态区域使用 RegisterViewWithRegion
或对动态区域使用导航服务,您需要使用导航服务更改视图。您可以导航到初始视图,而不是向区域管理器和 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''.
此绑定工作正常,由于上述 单个活动区域 问题而失败。两个视图都添加到 CombatWizardRegion
的 Views
集合,但只有第一个被添加到 ActiveViews
集合并显示,因此设置为相应 [=] 的 Content
16=]。因此,第一个视图在可视化树中并接收数据上下文,而第二个视图不在可视化树中,因此它的数据上下文是null
并且有没有祖先,这会导致错误。