In WPF, is UI immediately updated upon invoking PropertyChanged

Martin Han 21 Reputation points
2022-07-28T15:24:39.377+00:00

I have a ViewModel abstract class:

public abstract class ViewModel : INotifyPropertyChanged {  
    public event PropertyChangedEventHandler PropertyChanged;  
    protected void NotifyPropertyChanged([CallerMemberName] String propertyName = null) {  
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
    }  
    public bool IsEmpty { get; set; }  
}  

It is the base class of all my viewmodels, one of my viewmodels is this (the important part is PerFrameCallBack, you may not care about other code):

public class GameWorldViewModel : ViewModel, IDisposable {  
    public GameWorldViewModel(GameWorld gameWorld, Player player, InputBindingManager inputBindingManager) {  
        this.gameWorld = gameWorld;  
        this.player = player;  
        this.inputBindingManager = inputBindingManager;  
        Camera = new Camera(gameWorld);  
        Camera.Center = player.PlayerGameBody.Center;  
        eventHandler = new EventHandler(PerFrameCallback);  
        CompositionTarget.Rendering += eventHandler;  
    }  
    private readonly GameWorld gameWorld;  
    public Player player { get; }  
    private readonly InputBindingManager inputBindingManager;  
    public Camera Camera { get; }  
  
    public GameBodyCollectionViewModel GameBodyCollectionViewModel { get; } = new GameBodyCollectionViewModel();  
    public MinimapViewModel MinimapViewModel { get; } = new MinimapViewModel();  
    public PausePanelsViewModel PausePanelsViewModel { get; } = new PausePanelsViewModel();  
    public PlayerViewModel PlayerViewModel { get; } = new PlayerViewModel();  
    public FPSViewModel FPSViewModel { get; } = new FPSViewModel();  
  
    private readonly Stopwatch stopwatch = new Stopwatch();  
  
    private readonly EventHandler eventHandler;  
    private Task gameUpdateTask = Task.CompletedTask;  
    private void PerFrameCallback(object sender, EventArgs e) {  
        if (PausePanelsViewModel.IsPaused) {  
            stopwatch.Stop();  
            UserInputCollection.Clear();  
            return;  
        } else {  
            stopwatch.Start();  
        }  
        if (gameUpdateTask.IsCompleted) {//Check if the backgroud update has completed  
            FPSViewModel.PlusOneFrameCount();  
            UpdateDataFromModel();//Copy the data but not update UI  
            double deltaTime = stopwatch.Elapsed.TotalSeconds;  
            stopwatch.Restart();  
            gameUpdateTask = gameWorld.BeginUpdate(deltaTime, GetPlayerBehaviourUpdateData());//Let backgroud game engine calculate next frame of the game.  
            //gameUpdateTask = Task.CompletedTask;  
            NotifyChanged();//Update UI, runing concurrently with backgroud game engine thread.  
        }  
    }  
    public readonly Input.UserInputCollection UserInputCollection = new();  
    private PlayerBehaviourUpdateData[] GetPlayerBehaviourUpdateData() {  
        var result = new PlayerBehaviourUpdateData[] {  
            new PlayerBehaviourUpdateData() {  
                ToStartCastingAbilityIndexes = UserInputCollection.ToStartCastingIndexHashSet.ToArray(),  
                ToCancelCastingAbilityIndexes = UserInputCollection.ToCancelCastingIndexHashSet.ToArray(),  
                ToSetInputDatas = UserInputCollection.ToSetInputDataDictionary.Select(p => (p.Key, p.Value)).ToArray(),  
                ToSetMovementAction = UserInputCollection.ToSetMovementActionTuple,  
            }  
        };  
        UserInputCollection.Clear();  
        return result;  
    }  
    private void UpdateDataFromModel() {  
        if (Camera.NeedMoveTo != null) {  
            Camera.Center = Camera.NeedMoveTo.Center;  
            Camera.NeedMoveTo = null;  
        }  
        GameBodyCollectionViewModel.UpdateDataFromModel(gameWorld, Camera);  
        MinimapViewModel.UpdateDataFromModel(gameWorld, Camera);  
        PausePanelsViewModel.UpdateDataFromModel(gameWorld.EnemyWaveManager, player.Inventory);  
        PlayerViewModel.UpdateDataFromModel(player, inputBindingManager);  
    }  
  
    private void NotifyChanged() {  
        GameBodyCollectionViewModel.NotifyChanged();  
        MinimapViewModel.NotifyChanged();  
        PausePanelsViewModel.NotifyChanged();  
        PlayerViewModel.NotifyChanged();  
    }  
  
    public void Dispose() {  
        CompositionTarget.Rendering -= eventHandler;  
    }  
}  

As you can see, I seperately called UpdateDataFromModel and NotifyChanged. These viewmodels doesn't invoke their PropertyChanged event when their properties are set, but invoke when their NotifyChanged method are called.
For example:

public class MinimapViewModel : CollectionViewModel<MinimapItemViewModel, MinimapItem> {  
    public MinimapViewModel() {  
  
    }  
    public MinimapView View { get; set; }  
    public double ViewWidth => View.ActualWidth;  
    public double ViewHeight => View.ActualHeight;  
    public double OccupiedViewWidth { get; private set; }  
    public double OccupiedViewHeight { get; private set; }  
    public double CameraIndicatorLowerBoundX { get; private set; }  
    public double CameraIndicatorLowerBoundY { get; private set; }  
    public double CameraIndicatorWidth { get; private set;}  
    public double CameraIndicatorHeight { get; private set; }  
  
    public void UpdateDataFromModel(GameWorld gameWorld, Camera camera) {  
        double viewObjectRatioX = ViewWidth / gameWorld.Width;  
        double viewObjectRatioY = ViewHeight / gameWorld.Height;  
        double lowerRatio = Math.Max(viewObjectRatioX, viewObjectRatioY);  
        OccupiedViewWidth = gameWorld.Width * lowerRatio;  
        OccupiedViewHeight = gameWorld.Height * lowerRatio;  
        CameraIndicatorLowerBoundX = camera.LowerBound.X * lowerRatio;  
        CameraIndicatorLowerBoundY = camera.LowerBound.Y * lowerRatio;  
        CameraIndicatorWidth = camera.Width * lowerRatio;  
        CameraIndicatorHeight = camera.Height * lowerRatio;  
        UpdateDataFromModel_Protected(from g in gameWorld.GameBodies select new MinimapItem(g), (viewModel, model) => {   
            viewModel.UpdateDataFromModel(model, lowerRatio);  
        });  
    }  
  
    public override void NotifyChanged() {  
        base.NotifyChanged();  
        NotifyPropertyChanged(nameof(OccupiedViewWidth));  
        NotifyPropertyChanged(nameof(OccupiedViewHeight));  
        NotifyPropertyChanged(nameof(CameraIndicatorLowerBoundX));  
        NotifyPropertyChanged(nameof(CameraIndicatorLowerBoundY));  
        NotifyPropertyChanged(nameof(CameraIndicatorWidth));  
        NotifyPropertyChanged(nameof(CameraIndicatorHeight));  
    }  
}  

Each viewmodel have a corresponding view, which is deverived from content control and have their elements' propertys binding to viewmodel's fields. To be more focused on the keypoint, I am not pasting the code here.
I suspect that what I have done is actually not necessary. As WPF has the Dispatcher. Maybe when PropertyChanged event is invoked, the system just pend UI update task into the Dispatcher and not update UI imediately?
If its so, then I don't need to save the data first then let the GameWorld task run then NotifyChanged, and can implement my viewmodel classes normaly (invoking PropertyChanged event when property set is called).

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,676 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Hui Liu-MSFT 40,266 Reputation points Microsoft Vendor
    2022-08-02T08:20:55.263+00:00

    Hi,@Martin Han . When Controls has DependencyProperty binding to the property that implements the INotifyPropertyChanged interface, the UI is updated synchronously. Here is a simple example.

     <Window.DataContext>  
            <local:DocumentModel/>  
        </Window.DataContext>  
        <StackPanel>  
            <TextBox x:Name="tb" Background="LightGreen" Text="{Binding Name, Mode=TwoWay ,UpdateSourceTrigger=PropertyChanged}"/>  
            <TextBlock x:Name="tb1" Background="AliceBlue"  Text="{Binding Name, Mode=TwoWay ,UpdateSourceTrigger=PropertyChanged}"/>  
        </StackPanel>  
    

    Codebehind:

     public class DocumentModel : BaseModel  
        {  
            private string? name;  
            public string? Name  
            {  
                get { return name; }  
                set  
                {  
                    name = value;  
                    NotifyPropertyChanged("Name");  
                }  
            }  
        }  
        public class BaseModel : INotifyPropertyChanged  
        {  
            public event PropertyChangedEventHandler? PropertyChanged;  
            protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")  
            {  
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
            }  
           
        }  
    

    The result: The Text property of TextBlock is bound to the property Name, and when the value of Name is updated, the Text value of TextBlock is also updated.

    227192-image.png


    If the response is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.