HateDread
Topic Author
Posts: 72
Joined: 06 Feb 2020, 10:27

How best to split responsibility into multiple ViewModels/Views? Should I?

03 Oct 2023, 16:40

Hi,

I have a GameViewModel, which serves as the "main" view model for the game. It has a number of ViewModels on it, and is represented in the UI with a GameView.xaml UserControl, which has some direct UI elements it creates, but also binds properties of GameViewModel to e.g. ItemsControl widgets and so on.

I'm trying to figure out how "best" to split some data out of it. For example, I have a bunch of interaction with units in a game (or at least am planning to), for example hovering over points of interest leading to specific pop-ups, as well as various controls for making and modifying orders. I didn't want to put everything right onto GameView and in the GameView,xaml as that will get crazy. How does one split things out into more focused-responsibility view models?

First I was thinking of some over-arching view model like ClientOrderInteractionViewModel, and then somehow getting a xaml spawned to represent that, which can create and bind different views to the various view model properties that live on ClientOrderInteractionViewModel, e.g. properties like
List<ClientOrderViewModel> activeOrders;
that can be bound as data context to other xaml files.

I guess I'm just misunderstanding the glue here:
* How does GameView.xaml "spawn" these other views?
* Does it make sense to split into single-responsibility ViewModels like this for sanity? In a non-UI world I would definitely wouldn't put every single bit of data in one class
* In this example, ClientOrderInteractionView would itself do nothing but create and hold other views and bind their DataContexts; is this a normal use-case for a view? I feel like I'm going to have multiple full-sized views covering the entire application window that create different parts of the main UI kind of like layers, rather than it being in one integrated file. That wraps back around to feeling bad again, like the split isn't helpful.
* But is this missing the purpose of a view? I'm looking for some xaml to hold some of these bindings/widgets so I guess so? But a 'view' feels more like, I dunno, a button or a widget or something, not this vague container of other controls?

Any help would be appreciated understanding the approach/model here when getting into the weeds. I can handle basic game-level data like the name of the current world or whatever - that's pretty obvious for GameViewModel and is bound by some TextBlock widget, easy. But then getting more complicated and needing to split out and have all of the above, potentially... that's confusing as hell still.

Thanks!

EDIT: It had been a while and I was confused. I have remembered that I can set up a DataTemplate to "load" the correct view and display it, like so (in this case PredictionViewModel might be the thing that has orders as mentioned above, or another intermediary ViewModel between them)

In GameView.xaml (my main game View)
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type viewModels:ClientPredictionViewModel}">
            <views:ClientPredictionView/>
        </DataTemplate>
    </UserControl.Resources>
    <Grid>

        <ContentControl Content="{Binding PredictionViewModel}"></ContentControl>
I could see in this case, ClientPredictionView.xaml could itself have a bunch of DataTemplates like this, and serve to display and position sub-views that bind to ViewModels that exist on the ClientPredictionViewModel.

While that mechanism makes more sense to me now, the questions around what might be "best" or "standard" remain. Intermediary views xamls and ViewModels?
 
User avatar
sfernandez
Site Admin
Posts: 3008
Joined: 22 Dec 2011, 19:20

Re: How best to split responsibility into multiple ViewModels/Views? Should I?

03 Oct 2023, 20:03

Hi,

It is totally normal and a good practice to split your game data in different view models.

An application should expose a hierarchy of view models through properties and collections. And the UI can bind to those properties using ContentControls (as you already mentioned); and to the collections using any ItemsControl (or derived control).

You can define DataTemplates for specific DataTypes, or you can explicitly set the DataTemplate in ItemsControl through the ItemTemplate property, or in a ContentContorl through the ContentTemplate property. And the DataTemplate can define the visual tree directly
<DataTemplate x:Key="ClientOrderTemplate">
  <StackPanel Orientation="Horizontal">
    <TextBlock Text="{Binding Number}"/>
    <TextBlock Text="{Binding Name}"/>
  </StackPanel>
</DataTemplate>
or just include a new UserControl that will load a totally different XAML.
<DataTemplate x:Key="ClientOrderTemplate">
  <local:ClientOrderView/>
</DataTemplate>
I suggest you organize it this way because it would be easier to maintain and to iterate.
 
HateDread
Topic Author
Posts: 72
Joined: 06 Feb 2020, 10:27

Re: How best to split responsibility into multiple ViewModels/Views? Should I?

05 Oct 2023, 19:08

Hi,

It is totally normal and a good practice to split your game data in different view models.

An application should expose a hierarchy of view models through properties and collections. And the UI can bind to those properties using ContentControls (as you already mentioned); and to the collections using any ItemsControl (or derived control).

You can define DataTemplates for specific DataTypes, or you can explicitly set the DataTemplate in ItemsControl through the ItemTemplate property, or in a ContentContorl through the ContentTemplate property. And the DataTemplate can define the visual tree directly
<DataTemplate x:Key="ClientOrderTemplate">
  <StackPanel Orientation="Horizontal">
    <TextBlock Text="{Binding Number}"/>
    <TextBlock Text="{Binding Name}"/>
  </StackPanel>
</DataTemplate>
or just include a new UserControl that will load a totally different XAML.
<DataTemplate x:Key="ClientOrderTemplate">
  <local:ClientOrderView/>
</DataTemplate>
I suggest you organize it this way because it would be easier to maintain and to iterate.
Thanks, this helps a lot! I'm currently doing things the latter way, with a DataTemplate loading a view, and used with a ContentControl's Content property. How can I use this with DependencyProperties added to the code-behind of e.g. ClientOrderView in your latter example? I'm trying to get a click event from the ClientOrderView out to the view model associated with the view the xaml file your example would be in.

EDIT: This is how I ended up doing it, e.g. in ClientPredictionsView.xaml
            <DataTemplate DataType="{x:Type viewmodels:OrderCreationViewModel}">
                <local:OrderCreationView 
                    RouteOnOrderCreationClicked="{Binding DataContext.OrderCreationRequested, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:ClientPredictionsView}}"/>
            </DataTemplate>
Because the binding is from the OrderCreationView's POV, I guess the "current" view model needs to be found by traversing back up. Is there an easier way to reach into the view code-behind's DependencyProperty and bind it to our own DataContext (view model) callback function?
 
User avatar
sfernandez
Site Admin
Posts: 3008
Joined: 22 Dec 2011, 19:20

Re: How best to split responsibility into multiple ViewModels/Views? Should I?

06 Oct 2023, 19:01

When you want to call into the ViewModel you should use Commands.

In your View you will expose a command property that will be bound to your view model. Then the view just needs to execute the command and that will call the viewmodel as you want.
<DataTemplate x:Key="ClientOrderTemplate">
  <local:ClientOrderView SelectOrderCommand="{Binding SelectOrderCommand}"/>
</DataTemplate>
If the command is not exposed in the ClientOrderViewModel, but on a parent view model, you should be able to access it using ElementName (or RelativeSource as you did):
<ItemsControl x:Name="ClientOrders" ItemsSource="{Binding ClientOrders}" ItemTemplate="{StaticResource ClientOrderTemplate}">
<DataTemplate x:Key="ClientOrderTemplate">
  <local:ClientOrderView SelectOrderCommand="{Binding DataContext.SelectClientOrderCommand, ElementName=ClientOrders}"/>
</DataTemplate>
 
HateDread
Topic Author
Posts: 72
Joined: 06 Feb 2020, 10:27

Re: How best to split responsibility into multiple ViewModels/Views? Should I?

07 Oct 2023, 18:58

When you want to call into the ViewModel you should use Commands.

In your View you will expose a command property that will be bound to your view model. Then the view just needs to execute the command and that will call the viewmodel as you want.
<DataTemplate x:Key="ClientOrderTemplate">
  <local:ClientOrderView SelectOrderCommand="{Binding SelectOrderCommand}"/>
</DataTemplate>
If the command is not exposed in the ClientOrderViewModel, but on a parent view model, you should be able to access it using ElementName (or RelativeSource as you did):
<ItemsControl x:Name="ClientOrders" ItemsSource="{Binding ClientOrders}" ItemTemplate="{StaticResource ClientOrderTemplate}">
<DataTemplate x:Key="ClientOrderTemplate">
  <local:ClientOrderView SelectOrderCommand="{Binding DataContext.SelectClientOrderCommand, ElementName=ClientOrders}"/>
</DataTemplate>
I've been using the ContentControl approach (and the relative bindings) to success so far, though haven't tried your other example yet; am bogged down elsewhere with this approach.
<Style x:Key="ProjectedPointCenteredControlStyle" TargetType="{x:Type ContentControl}">
	<Setter Property="controls:CenteredCanvas.X" Value="{Binding HoveredPoint.ProjectedPointX}"/>
	<Setter Property="controls:CenteredCanvas.Y" Value="{Binding HoveredPoint.ProjectedPointY}"/>
</Style>


<DataTemplate DataType="{x:Type viewmodels:HoveredPredictionPointViewModel}">
    <local:ClientPredictionHoveredPointPopupView />
</DataTemplate>

<!-- We need to bind the order creation view to be on the HoveredPoint, which is only available in our VM, so we need to traverse back up to ourselves in the binding -->
<Style x:Key="OrderCreationCenteredControlStyle" TargetType="{x:Type ContentControl}">
	<Setter Property="controls:CenteredCanvas.X" Value="{Binding DataContext.HoveredPoint.ProjectedPointX, 
		RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:ClientPredictionsView}}"/>
	<Setter Property="controls:CenteredCanvas.Y" Value="{Binding DataContext.HoveredPoint.ProjectedPointY, 
		RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:ClientPredictionsView}}"/>
</Style>

<ContentControl 
	Content="{Binding HoveredPoint}"
	Style="{StaticResource ProjectedPointCenteredControlStyle}"
/>
But I wanted to have this ClientPredictionHoveredPointPopupView, which is a view on top of the VM HoveredPredictionPointViewModel, be not present/exist when that view model is null (exposed as property "HoveredPoint") without any errors etc.

I've tried many approaches, like switching the DataTemplate on the validity of HoveredPoint in a style's triggers (I have an empty "EmptyTemplate" data template to fall back on), but it starts getting pretty funky trying to use a DataTemplate and a Style and a ContentControl (DataTemplate vs Template? Etc). Example:
<Style x:Key="ProjectedPointCenteredControlStyle" TargetType="{x:Type ContentControl}">
	<Setter Property="controls:CenteredCanvas.X" Value="{Binding HoverPoint.ProjectedPointX}"/>
	<Setter Property="controls:CenteredCanvas.Y" Value="{Binding HoverPoint.ProjectedPointY}"/>
	<Setter Property="Template" Value="{StaticResource SomeHoverPointControlTemplateHere}"/>
	<Style.Triggers>
		<DataTrigger Binding="{Binding DataContext.HoverPoint, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:ClientPredictionsView}, 
		Converter={StaticResource IsNullConverter}}" Value="True">
			<Setter Property="Template" Value="{StaticResource EmptyTemplate}"></Setter>
		</DataTrigger>
	</Style.Triggers>
</Style>
But even if I can get it to disappear when the VM is invalid, doing it this way without DataTemplate means its own DataContext isn't correct (whereas using ContentControl's Content property + setting up a DataTemplate on a type that the content will be set to just... works. But can't see how to if-else that on a trigger).

Even when I find an approach that works in C#/Blend, the C++ side spams the log about HoveredPoint being nullptr. Does Noesis allow for nullptr properties? Is that some other value in C++ I should use when I want to "null out" a Noesis::Ptr<ViewModelTypeHere> property without log spam?

EDIT: I tried something that I thought would work (nulling out the Content of the ContentControl), but same issue - same log spam on HoveredPoint returning nullptr, and based on other logged errors it looks like the View I'm trying to prevent from being made still is. Thinking nullptr isn't a valid type for Noesis C++
<local:OrderInteractionPointView/>
	<DataTemplate DataType="{x:Type viewmodels:HoveredPredictionPointViewModel}">
</DataTemplate>

<Style x:Key="ProjectedPointCenteredControlStyle" TargetType="{x:Type ContentControl}">
	<Setter Property="controls:CenteredCanvas.X" Value="{Binding HoveredPoint.ProjectedPointX}"/>
	<Setter Property="controls:CenteredCanvas.Y" Value="{Binding HoveredPoint.ProjectedPointY}"/>
	<Setter Property="Content" Value="{x:Null}"></Setter>
	<Style.Triggers>
		<DataTrigger Binding="{Binding DataContext.HoveredPoint, 
			RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:ClientPredictionsView}, 
			Converter={StaticResource IsNullConverter}}" Value="False">
			<Setter Property="Content" Value="{Binding HoveredPoint}"/>
		</DataTrigger>
	</Style.Triggers>

</Style>

</Grid.Resources>

<Canvas HorizontalAlignment="Left" VerticalAlignment="Top">
	<Canvas.Children>
		<ContentControl Style="{StaticResource ProjectedPointCenteredControlStyle}"/>
	</Canvas.Children>
</Canvas>
 
User avatar
sfernandez
Site Admin
Posts: 3008
Joined: 22 Dec 2011, 19:20

Re: How best to split responsibility into multiple ViewModels/Views? Should I?

10 Oct 2023, 20:02

I think the binding errors are coming from the ContentControl style setters, not from the inner view itself.
Anyway, you can have a control that shows the inner view along with the Canvas only when the view model is not null:
<ControlTemplate x:Key="HoveredPointPopupTemplate" TargetType="Control">
  <Canvas HorizontalAlignment="Left" VerticalAlignment="Top">
    <ContentControl Content="{Binding HoveredPoint}"
                    controls:CenteredCanvas.X="{Binding HoveredPoint.ProjectedPointX}"
                    controls:CenteredCanvas.Y="{Binding HoveredPoint.ProjectedPointY}"/>
  </Canvas>
</ControlTemplate>
<Style x:Key="HoveredPointPopupStyle" TargetType="Control">
  <Setter Property="Template" Value="{StaticResource HoveredPointPopupTemplate}"/>
  <Style.Triggers>
    <DataTrigger Binding="{Binding HoveredPoint}" Value="{x:Null}">
      <Setter Property="Template" Value="{x:Null}"/>
    </DataTrigger>
  </Style.Triggers>
</Style>
...
<Control Style="{StaticResource HoveredPointPopupStyle}"/>
Is this what you are looking for?

Who is online

Users browsing this forum: No registered users and 3 guests