Laden...

Prism Navigation: Region liegt in anderem Projekt

Erstellt von DeSharper vor 5 Jahren Letzter Beitrag vor 5 Jahren 2.330 Views
D
DeSharper Themenstarter:in
40 Beiträge seit 2016
vor 5 Jahren
Prism Navigation: Region liegt in anderem Projekt

Hallo zusammen,
ich habe ein Problem bei der Navigation mit Prism 7 und habe den starken Verdacht, dass das an meiner Projekt-Struktur liegt. Mein Startup-Projekt enthält die App.xaml.cs. Alle Views befinden sich in einem anderen Projekt auch MainWindowShell. MainWindowShell enthält eine ContentRegion in der Anfangs MainMenuView liegen soll. Über einen Command soll im MainMenuViewModel zum GraphicMenuView navigiert werden. Also statt MainMenuView soll in der Region GraphicMenuView liegen Beim Debuggen habe ich festgestellt, dass der Command auch durchlaufen wird. Das NavigationResult ist false aber ohne Error und auf der Oberfläche passiert gar nichts. Beim Vergleich mit funktionierendem Code ist mir aufgefallen, dass bei mir im regionManager keine Regions sind, obwohl ich in der MainWindowShell eine Region angelegt habe. Bei ausführlichen Tests habe ich festgestellt, dass, wenn ich MainWindowShell in mein Startup-Projekt verschiebe immerhin navigiert wird. Kann es sein, dass ich wegen zwei unterschiedlicher Projekte zwei RegionManager habe und deshalb auch keine Region in dem Startup-Projekt-RegionManager ist? Wenn ja, wie kann ich das beheben. Ich habe auch schon erfolglos versucht die Views aus dem ViewProjekt in einem IModul hinzuzufügen. Es hat mein Problem nicht gelöst. Ich bin langsam ratlos. Das einzige, was mir einfällt, um mein Problem zu lösen, wäre den Inhalt des View-Projekts ins Startup-Projekt zu verschieben. Aber das möchte ich eigentlich nicht nur machen müssen, nur weil ich zu blöd bin es anders zu lösen. Ich bin für alle Hinweise dankbar.


    public partial class App : PrismApplication
    {
        protected override void InitializeShell(Window shell)
        {
            var mainWindow = (MainWindowShell)shell;

            var regionManager = Container.Resolve<IRegionManager>();
            regionManager.RegisterViewWithRegion(RegionNames.Content, typeof(MainMenuView));

            this.MainWindow = mainWindow;
            mainWindow.Show();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<GraphicMenuView>();
            containerRegistry.RegisterForNavigation<MainMenuView>();
        }

        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindowShell>();
        }
    }

MainWindowShell (Auszug):


<ContentControl regions:RegionManager.RegionName="{x:Static utilities:RegionNames.Content}">

MainWindowView (Auszug):


<Button Command="{Binding NavigateCommand}" CommandParameter="Graphic"/>

1.040 Beiträge seit 2007
vor 5 Jahren

Wahrscheinlich fehlt irgendwo eine Verknüpfung oder der Name der Region ist evtl. unvollständig?

Um auszuschließen, dass es an deiner Projektstruktur liegt, empfehle ich dir den Link:
[Tutorial] Vertrackte Fehler durch Vergleich von echtem Projekt mit minimalem Testprojekt finden

T
18 Beiträge seit 2009
vor 5 Jahren

Views und ViewModels sollten auch aus einem anderen Projekt (dll) zu laden sein. Hast du dieses dem "Hauptprogramm" per ModuleCatalog bekannt gemacht? Dort dann auch gleich die Views per

containerRegistry.RegisterForNavigation<ViewName>(eindeutiger_string_für_view)

registriert?

Zu "RegisterTypes" deiner "PrismApplication":
Versuch doch mal, den beiden RegisterForNavigation-Methoden einen eindeutigen String für die spätere Navigation mitzugeben. Der sollte dann, wenn navigiert werden soll, für die RequestNavigate-Methode des RegionManagers erforderlich sein.

"InitializeShell":
Hier registrierst du Sachen, die nichts mit der eigentlichen Initialisierung der Shell zu tun haben, oder? Bei meinen "Projekten" (mega-hobbymäßig, hab's weder studiert noch arbeite ich in ner Softwareschmiede) fehlt die Implementierung komplett, das überlasse ich Prism.

Hier nochmal Beispielcode für die RegisterTypes-Implementierung und der OnInitialized-Impl. (wird aufgerufen, wenn "alle anderen Sachen" erledigt sind und das Window/die ShellView angezeigt wird):

    public partial class App : PrismApplication
    {
        // Modulregistrierung per App.config
        protected override IModuleCatalog CreateModuleCatalog()
        {
            return new ConfigurationModuleCatalog();
        }

        protected override Window CreateShell()
        {
            return Container.Resolve<ShellView>();
        }

        protected override void OnInitialized()
        {
            base.OnInitialized();

            var regionManager = Container.Resolve<IRegionManager>();
            // im "leeren" Window wird DashboardView als "erster Content" geladen/angezeigt
            regionManager.RequestNavigate(RegionNames.ShellRegion, ViewContentNames.DashboardView);
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.Register<IDialogCoordinator, DialogCoordinator>();

            containerRegistry.Register<ShellView>();
            containerRegistry.Register<ShellViewModel>();
        }
    }

ViewContentNames enthält statische Strings für die View-Bezeichner, damit sich keine Rechtschreibfehler einschleichen - du kannst natürlich auch einfach Strings nehmen.

Views aus anderen Modulen kannst du dann per registrieren (und mit ihrem Namen ansprechen), wenn sie registriert wurden:

    public class UIModule : IModule
    {
        public void OnInitialized(IContainerProvider containerProvider)
        {
            //
        }

        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<DashboardView>(ViewContentNames.DashboardView);
            containerRegistry.RegisterForNavigation<CustomersView>(ViewContentNames.CustomersView);
            containerRegistry.RegisterForNavigation<CustomerAddEditView>(ViewContentNames.CustomerAddEditView);
            containerRegistry.RegisterForNavigation<SettingsView>(ViewContentNames.SettingsView);
        }
    }

Hoffe, das hilft dir ein wenig weiter. Falls nicht, kannst du ja vielleicht eine abgespeckte Variante deines Programms oder nen bissl mehr Code der Implementierungen einstellen, dann sieht man mehr vom Ablauf.

LG Tris

D
DeSharper Themenstarter:in
40 Beiträge seit 2016
vor 5 Jahren

Ich wollte keine Codewüste posten, aber wenn es hilft:
App.xaml.cs im Startup-Projekt


    public partial class App : PrismApplication
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindowShell>();
        }

        protected override void InitializeShell(Window shell)
        {
            var mainWindow = (MainWindowShell)shell;
            MainWindow = mainWindow;
            mainWindow.Show();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterSingleton<IMainMenuViewModel, MainMenuViewModel>();
        }

        protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
        {
            moduleCatalog.AddModule<ViewModul>();
        }
    }

Modul im View-Projekt


    public class ViewModul : IModule
    {
        private readonly IRegionManager _regionManager;

        public ViewModul(IRegionManager regionManager)
        {
            _regionManager = regionManager;
        }

        public void OnInitialized(IContainerProvider containerProvider)
        {
            _regionManager.RequestNavigate(RegionNames.Content, ViewNames.GraphicMenu);
        }

        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<GraphicMenu>(ViewNames.GraphicMenu);
            containerRegistry.RegisterForNavigation<MainMenu>(ViewNames.MainMenu);

            _regionManager.RegisterViewWithRegion(RegionNames.Content, typeof(MainMenu));
        }
    }

Shell.xaml im View-Projekt (liegt in einem ResourceDictionary)


  <Style TargetType="{x:Type userControls:MainWindowShell}">
    <Setter Property="Template" Value="{DynamicResource MainWindowShell}" />
  </Style>

  <ControlTemplate x:Key="MainWindowShell" TargetType="{x:Type userControls:MainWindowShell}">
      <Grid>
      <ContentControl regions:RegionManager.RegionName="{x:Static utilities:RegionNames.Content}">
      </ContentControl>
      </Grid>
  </ControlTemplate>

MainWindowShell.cs im View-Projekt


    public class MainWindowShell : Window
    {
        static MainWindowShell()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MainWindowShell), new FrameworkPropertyMetadata(typeof(MainWindowShell)));
        }

        public MainWindowShell(IMainMenuViewModel viewModel)
        {
            this.DataContext = viewModel;
        }
    }

MainMenu.xaml im View-Projekt (liegt in einem ResourceDictionary)


  <Style TargetType="{x:Type pages:MainMenu}">
    <Setter Property="Template" Value="{DynamicResource MainMenu}"/>
  </Style>

  <ControlTemplate x:Key="MainMenu" TargetType="{x:Type pages:MainMenu}">
    <Grid >
      <Button Content="Grafikoptionen" 
              Command="{Binding NavigateCommand}" 
              CommandParameter="{x:Static utilities:ViewNames.GraphicMenu}"/>
    </Grid>
  </ControlTemplate>

MainMenu.cs im View-Projekt


    public class MainMenu : Control
    {
        static MainMenu()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MainMenu), new FrameworkPropertyMetadata(typeof(MainMenu)));
        }

        public MainMenu(IMainMenuViewModel viewModel)
        {
            this.DataContext = viewModel;
        }
    }

Um Vertipper auszuschließen habe ich jetzt sowohl den Namen der Region als auch die Bezeichner für die Views in statische Klassen ausgelagert. Die Registrierung der Views ist denke ich auch in Ordnung. Ich habs jetzt nochmal mit deinem Beispiel verglichen, aber du machst ja auch nichts anders, außer dass du eindeutige Bezeichner verwendest. Das habe ich ergänzt.
Ich habe auch schon versucht die Navigation ins OnInitialized des Moduls zu verschieben, hat auch nichts geholfen. Ich weiß langsam echt nicht mehr weiter. Ich kann das Projekt auch nicht mehr weiter abspecken, ist ja nix mehr drin.

"InitializeShell":
Hier registrierst du Sachen, die nichts mit der eigentlichen Initialisierung der Shell zu tun haben, oder?

Ich weiß nicht, ob ich dich hier richtig verstehe. Wenn du ein Wpf-Projekt frisch anlegst, dann wird in der app.xaml schon ein MainWindow angelegt, das initial auch angezeigt wird. Vermutlich ist bei dir deswegen InitializeShell leer. Das funktioniert aber meines Wissens nur, solange der Konstruktor dieses MainWindows parameterlos ist. Das ist bei mir nicht so, weil der sein zugehöriges ViewModel im Konstruktor erwartet. Deswegen muss vorher der ganze DependencyInjection-Kladderadatsch durchlaufen.

Kannst du bei deinem Projekt mal schauen, wenn du einen Breakpoint vor RequestNavigate setzt, ob in dem Moment bei dir in regionManager.Regions was drinsteht oder ob die Liste leer ist? Ich bin mir nicht sicher, ob das so sein müsste, kommt mir aber schon sinnvoll vor. Bei mir ist das jedenfalls immer leer.

T
18 Beiträge seit 2009
vor 5 Jahren

Zuerst einmal muss die "OnInitialized"-Methode aus dem ViewModul in deine app.xaml.cs. Dort dann allerdings parameterlos und als override stehen.

Module-Klassen (abgeleitet von IModule) dienen theoretisch der Entkopplung der Programmbausteine und sind für die Bekanntmachung ihrer (internen) Klassen/Interfaces/Views/Whatever beim IoC/DI-Container verantwortlich. Navigation(Requests) kommen erst viel später in der Kette. Siehe auch Prism Library - Initializing

Auch solltest du die Injizierung des ViewModels (erfolgt bei dir noch manuell in "MainWindowShell.cs") dem Prism-Framework überlassen. Hier gibt es ein Attachend Property namens "AutoWireViewModel", welches in der View (xaml) auf true zu setzen ist. Prism bringt einen eigenen ViewModelLocator mit, der anhand der Namensgebung das ViewModel ermittelt und an DataContext bindet (hierbei werden auch gleich weitere Abhängigkeiten - Konstruktorparameter - aufgelöst und injiziert), siehe Prism Library - ViewModelLocator

"InitializeShell":
Hier registrierst du Sachen, die nichts mit der eigentlichen Initialisierung der Shell zu tun haben, oder?
Ich weiß nicht, ob ich dich hier richtig verstehe. Wenn du ein Wpf-Projekt frisch anlegst, dann wird in der app.xaml schon ein MainWindow angelegt, das initial auch angezeigt wird. Vermutlich ist bei dir deswegen InitializeShell leer. Das funktioniert aber meines Wissens nur, solange der Konstruktor dieses MainWindows parameterlos ist. Das ist bei mir nicht so, weil der sein zugehöriges ViewModel im Konstruktor erwartet. Deswegen muss vorher der ganze DependencyInjection-Kladderadatsch durchlaufen.

Der Verweis auf die StartupURI muss aus der app.xaml raus. Durch die Vererbung von PrismApplication in der app.xaml.cs (die bei Programmstart ja zuerst ausgeführt wird) ist das gleichzeitig der Bootstrapper (s. ersten Link), der dir das Grund-Start-Gerüst deiner Anwendung zusammensetzt und als Abschluss eine Shell (Window) anzeigt.

Kannst du bei deinem Projekt mal schauen, wenn du einen Breakpoint vor RequestNavigate setzt, ob in dem Moment bei dir in regionManager.Regions was drinsteht oder ob die Liste leer ist? Ich bin mir nicht sicher, ob das so sein müsste, kommt mir aber schon sinnvoll vor. Bei mir ist das jedenfalls immer leer.

Hab einen Eintrag drin stehen, der für die ShellRegion.

Grad bei so umfänglichen Frameworks, wie auch Prism, ist die Dokumentation und der Beispielcode echt Gold wert. Hast du Prism Library - Dokumentation schon intensiv durchgearbeitet?

LG Tris

D
DeSharper Themenstarter:in
40 Beiträge seit 2016
vor 5 Jahren

Wenn ich OnInitialized aus dem ViewModul rausnehme wird das Interface "IModule" nicht mehr implementiert. Das RequestNavigate habe ich in beiden OnInitialized-Methoden ausprobiert. Hab ich bei dir gesehen und dachte, das spart mir vielleicht das klicken beim Testen 😃

Zu "InitializeShell": Jetzt verstehe ich, was du meinst. Ich habe einfach eine persönliche Abneigung gegen "Autowire", weil es mir nicht gefällt, wenn meine Klassen irgendwelchen Namenskonventionen entsprechen müssen. Als ich mit Prism angefangen hab, hat das auch nicht so richtig funktioniert, kann aber sein, dass ich damals noch andere Probleme drin hatte. Jedenfalls hab ich dann das AutoWire rausgeschmissen und mich nicht mehr weiter mit dem Feature beschäftigt. Aber davon abgesehen wird ja auch alles richtig verdrahtet. Die Views haben alle die richtigen ViewModels und ich laufe beim Debuggen ja auch in den Command-Code rein. Nur das RequestNavigate funktioniert nicht, möglicherweise weil bei mir NICHTS in regionManager.Regions steht.

Die Doku nutze ich natürlich, insbesondere den Teil zur Navigation. Aber ich finde einfach keinen Grund, warum es bei mir nicht funktioniert.
Was mich interessieren würde wäre, wann und wie die Region dem regionManager hinzugefügt wird, denn genau das scheint ja bei mir nicht zu passieren. Aber darüber hab ich in der Doku noch nichts gefunden. Und was ich auch nicht verstehe, wenn es die region gar nicht gibt, wieso kann dann überhaupt initial was angezeigt werden?

D
DeSharper Themenstarter:in
40 Beiträge seit 2016
vor 5 Jahren

Problem gefunden, es lag daran, dass ich CustomControls verwende, die keinen eigenen xaml-Code haben, sondern diesen immer per ControlTemplate aus dem ResourceDictionary bekommen (siehe Code oben).
Deswegen muss ich den RegionManager von Hand nochmal setzen und zwar erst dann, wenn das Template tatsächlich angewendet wird:


    public class MainWindowShell : Window
    {
        static MainWindowShell()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MainWindowShell), new FrameworkPropertyMetadata(typeof(MainWindowShell)));
        }

        private IRegionManager _regionManager;

        public MainWindowShell(IMainMenuViewModel viewModel, IRegionManager regionManager)
        {
            this.DataContext = viewModel;
            _regionManager = regionManager;
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            var contentControl = GetTemplateChild("ShellContent");
            RegionManager.SetRegionManager(contentControl, _regionManager);

        }
    }

Natürlich muss der Name "ShellContent" im Template auch gesetzt sein, sonst fliegt hier ne Exception.

Übrigens ist das Hinzufügen des Projekts zum ModuleCatalog nicht notwendig gewesen. Das geht auch ohne.