WPF - Prism 区域问题和祖先绑定的相关来源

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

我试图创建一个简单的问题示例:

假设我们有以下 UserControl,其中 LabelImage、区域和 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属性上。这两个视图也有一个唯一的标签:SpellWeapon 来区分他们两个。

然后我注册 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.

您的 CombatWizardRegionContentControl 中,它在默认 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''.

此绑定工作正常,由于上述 单个活动区域 问题而失败。两个视图都添加到 CombatWizardRegionViews 集合,但只有第一个被添加到 ActiveViews 集合并显示,因此设置为相应 [=] 的 Content 16=]。因此,第一个视图在可视化树中并接收数据上下文,而第二个视图不在可视化树中,因此它的数据上下文是null并且有没有祖先,这会导致错误。