Cordt
Cordt

Reputation: 59

WPF Caliburn Micro Notify Property Change not updating UI in ShellView

I created a new C# WPF project based on https://www.codemag.com/Article/1905031/A-Design-Pattern-for-Building-WPF-Business-Applications-Part-1 with Caliburn Micro and Windsor Castle based on .NET 7.0 as target framework. The Problem is: None of the Property Changes is updating the UI (The ugly gray InfoMessageArea should be hidden and a Text should appear while starting the App - as well as the WindowTitle that should reflect a change to "Test"). What am I missing here? The left list box correctly loads the implemented Modules using Windsor Castle, but the basic binding stuff is not working. And I would appreciate if anybody has an idea how the content of the window would "stretch" to full window's size as a side node. I created a new ShellView, that is acting as the Main Window of the application. The ShellViewModel derives from Conductor which has an ancestor Caliburn.Micro.PropertyChangedBase implementing INotifyPropertyChangedEx. Application's Window The ShellView.xaml code is as follows:

<Window x:Class="BM.App.Views.ShellView"
    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:vm="clr-namespace:BM.App.ViewModels"
    xmlns:local="clr-namespace:BM.App.Views"
    mc:Ignorable="d"
    d:DataContext="{x:Type vm:ShellViewModel}"
    Title="{Binding WindowTitle}"
    Height="450" Width="800"
    WindowStartupLocation="CenterScreen"
    Loaded="Window_Loaded">
<Window.Resources>
    <vm:ShellViewModel x:Key="viewModel" 
                      InfoMessageTitle="Please Wait While Loading Application..."
                      InfoMessage="Sample of Business Application Screens" />
</Window.Resources>

<Grid Style="{StaticResource contentAreaStyle}">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="20"/>
    </Grid.RowDefinitions>

    <ListBox x:Name="MenuItems" DisplayMemberPath="Caption" Grid.Column="0"/>

    <ContentControl x:Name="CurrentView" Grid.Row="0" Grid.Column="1" Margin="2"/>

    <!-- Informational Message Area -->
    <Border Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="1" Panel.ZIndex="2"
        Visibility="{Binding Path=IsInfoMessageVisible, Converter={StaticResource visibilityConverter}}" Style="{StaticResource infoMessageArea}">
        <StackPanel>
            <TextBlock FontSize="20" Text="{Binding Path=InfoMessageTitle}" />
            <TextBlock FontSize="12" Text="{Binding Path=InfoMessage}" />
        </StackPanel>
    </Border>

    <StatusBar x:Name="stbMain" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" MinWidth="500"></StatusBar>

</Grid>

The ShellView.xaml.cs as follows:

    public partial class ShellView : Window
{
    // Main window's view model class
    private ShellViewModel _viewModel;

    public ShellView()
    {
        InitializeComponent();
        // Connect to instance of the view model created by the XAML
        _viewModel = (ShellViewModel)this.Resources["viewModel"];
    }

    private async void Window_Loaded(object sender, RoutedEventArgs e)
    {
        await LoadApplication();

        // Turn off informational message area
        _viewModel.ClearInfoMessages();
    }

    public async Task LoadApplication()
    {
        // INFO: This text is not visible in the InfoMessage Area
        _viewModel.InfoMessage = "Loading application (method 1)...";

        //INFO: the 5 seconds delay are successfully invoked.
        //TODO: Load Stuff...
        await Dispatcher.BeginInvoke(new Action(() => {
            System.Threading.Thread.Sleep(5000);
        }), DispatcherPriority.Background);

        // INFO: Test will not be visible in the application's window!
        _viewModel.WindowTitle = "Test";

        // INFO: Neither Refresh() is updating the UI.
        _viewModel.Refresh();

        await Task.CompletedTask;
    }
}

The ShellViewModel.cs Code:

using BM.Contracts; 
using Caliburn.Micro; 
using System.Collections.ObjectModel; 
using System.Windows.Input; 
namespace BM.App.ViewModels 
{
    public class ShellViewModel : Conductor<object>
    {
        public ObservableCollection<ViewModelBase> OpenTabs { get; private set; } = new ObservableCollection<ViewModelBase>();

        public ObservableCollection<ShellMenuItem> MenuItems { get; private set; } = new ObservableCollection<ShellMenuItem>();

        private const string WindowTitleDefault = "mp Beleg Matching (LF | DATEV | SB)";
        private string _windowTitle = WindowTitleDefault;

        public ShellViewModel()
        {
            MenuItems = new ObservableCollection<ShellMenuItem>();
            AddTabCommand = null;
            CloseTabCommand = null;
            _selectedMenuItem = null;
        }
        ICommand AddTabCommand;
        ICommand CloseTabCommand;
        private bool _IsInfoMessageVisible = true;
        private string _InfoMessageTitle = string.Empty;
        private string _InfoMessage = string.Empty;

        public bool IsInfoMessageVisible
        {
            get { return _IsInfoMessageVisible; }
            set
            {
                _IsInfoMessageVisible = value;
                NotifyOfPropertyChange("IsInfoMessageVisible");
            }
        }

        public string InfoMessage
        {
            get { return _InfoMessage; }
            set
            {
                _InfoMessage = value;
                if (!_IsInfoMessageVisible && !string.IsNullOrEmpty(value))
                {
                    IsInfoMessageVisible = true;
                }
                NotifyOfPropertyChange("InfoMessage");
            }
        }

        public string InfoMessageTitle
        {
            get { return _InfoMessageTitle; }
            set
            {
                _InfoMessageTitle = value;
                NotifyOfPropertyChange("InfoMessageTitle");
            }
        }

        public void ClearInfoMessages()
        {
            InfoMessage = string.Empty;
            InfoMessageTitle = string.Empty;
            IsInfoMessageVisible = false;
        }

        public string WindowTitle
        {
            get { return _windowTitle; }
            set
            {
                _windowTitle = value;
                NotifyOfPropertyChange(() => WindowTitle);
            }
        }


        private ShellMenuItem _selectedMenuItem;

        public ShellMenuItem SelectedMenuItem
        {
            get { return _selectedMenuItem; }
            set
            {
                if (_selectedMenuItem == value)
                    return;
                _selectedMenuItem = value;
                NotifyOfPropertyChange(() => SelectedMenuItem);
                NotifyOfPropertyChange(() => CurrentView);
            }
        }

        public object? CurrentView
        {
            get { return _selectedMenuItem?.ScreenViewModel; }
        }
    }
}

Upvotes: 0

Views: 303

Answers (2)

Cordt
Cordt

Reputation: 59

So, after investigating quite some time, I found the solution to my problem. Hope this will help some other desperate souls out there, having the same binding problems:

I added the DataContext attribute to the Grid of the ShellView's XAML:

    <Grid Style="{StaticResource contentAreaStyle}"
      DataContext="{StaticResource viewModel}">

and instantly the Binding was working. What I also figured out: The Intellisense (Jump to Declaration/F12) didn't work on my bound properties. After adding the DataContext to the Grid it "magically" worked out (eg. hitting F12 on IsInfoMessageVisible jumped to the definition of the ViewModel). I am not an expert in WPF, so I wouldn't have come to the idea to add the DataContext a second time to a child control a second time if I even had a DataContext bound to the Window's container. May be one of you have some ideas to fill the gap between my ears :-).

Upvotes: 0

Frenchy
Frenchy

Reputation: 17037

You do a mixture with code behind and MVVM. If you are using Caliburn, use MVVM. Castle Windsor is a container, so i dont know, i am using Ninject and Caliburn (last version).

Sample to display a splash screen during 3 sec and change the title of ShellViewModel: just create a WPF .NET project then:

Modify App.xaml:

<Application x:Class="WpfApp2.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp2">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <local:Bootstrapper x:Key="Bootstrapper" />
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Bootsrapper.cs:

using Caliburn.Micro;
using Ninject;
using System;
using System.Collections.Generic;
using System.Windows;

namespace WpfApp2
{
    public class Bootstrapper : BootstrapperBase
    {
        private IKernel kernel;
        public Bootstrapper() //Constructor
        {
            Initialize();
        }
        protected override void Configure()
        {
            //Bind all views or tools to container
            kernel = new StandardKernel();
            kernel.Bind<IWindowManager>().To<WindowManager>().InSingletonScope();
            kernel.Bind<IEventAggregator>().To<EventAggregator>().InSingletonScope();
            kernel.Bind<SplashViewModel>().ToSelf().InSingletonScope();
            kernel.Bind<ShellViewModel>().ToSelf().InSingletonScope();
        }
        //Override the startup method and launch the shell instead
        protected override async void OnStartup(object sender, StartupEventArgs e)
        {
            var wm = kernel.Get<WindowManager>();
            var vm = kernel.Get<SplashViewModel>();
            //Load the  splash screen
            await wm.ShowWindowAsync(vm);
            
            //Load the ShellView
            await DisplayRootViewForAsync<ShellViewModel>();

        }
        protected override object GetInstance(Type service, string key)
        {
            return kernel.Get(service);
        }

        protected override IEnumerable<object> GetAllInstances(Type service)
        {
            return kernel.GetAll(service);
        }
    }
}

SplashView.xaml:

<Window x:Class="WpfApp2.SplashView"
        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:WpfApp2"
        mc:Ignorable="d" WindowStartupLocation="CenterScreen" WindowStyle="None"
        Title="SplashView" Height="100" Width="800">
    <Grid>
        <TextBox x:Name="Info" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
                 FontWeight="ExtraBold" FontSize="20" Foreground="Blue" IsReadOnly="True"/>
    </Grid>
</Window>

SplashViewModel.cs

using Caliburn.Micro;
using System.Threading;
using System.Threading.Tasks;

namespace WpfApp2
{
    public class SplashViewModel : Screen, IHandle<string>
    {
        public string Info { get; set; }
        public SplashViewModel(IEventAggregator eventAggregator)
        {
            Info = "Loading application (method 1)...";
            eventAggregator.SubscribeOnPublishedThread(this);
        }

        public async Task HandleAsync(string message, CancellationToken cancellationToken)
        {
            //Message received, wait 3 sec before to close the Splash Screen
            await Task.Delay(3000);
            if(message.Equals("End"))
                await TryCloseAsync();
        }
    }
}

ShellView.xaml

<Window x:Class="WpfApp2.ShellView"
        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:WpfApp2"
        mc:Ignorable="d" WindowStartupLocation="CenterScreen"
        Height="450" Width="800">
    <Grid>
        <TextBox Name="Info2" Width="300" Height="70" Background="Aquamarine" />
    </Grid>
</Window>

And finally ShellViewModel.cs

using Caliburn.Micro;
using System.Threading;
using System.Threading.Tasks;

namespace WpfApp2
{
    public class ShellViewModel : Screen
    {
        public IEventAggregator eventAggregator;

        public ShellViewModel(IEventAggregator eventAggregator)
        {
            this.eventAggregator = eventAggregator;
            //Change the title of view
            DisplayName = "TEST";
            Info2 = $"View {DisplayName} Loaded!!";
        }
        protected override async Task OnInitializeAsync(CancellationToken c)
        {
            //Publish a string message to close the Splash Screen
            await eventAggregator.PublishOnUIThreadAsync("End");
        }

        private string info2;
        public string Info2
        {
            get
            {
                return info2;
            }
            set
            {
                info2 = value;
                NotifyOfPropertyChange(() => Info2);
            }
        }
    }
}

Upvotes: 0

Related Questions