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).