[Released] Aspid.MVVM

Hello everyone!
I’m excited to introduce Aspid.MVVM — a powerful and high-performance MVVM framework for Unity, designed to simplify the creation of scalable and maintainable projects. It eliminates the use of reflection, minimizes memory allocations, and removes the need for boilerplate code, ensuring fast and optimized development.

Key Features of Aspid.MVVM

  • Data Binding: Supports four data binding modes (OneWay, TwoWay, OneTime, OneWayToSource) without reflection, delivering high performance.
  • ViewModel: Easily create ViewModels without inheriting from base classes or writing excessive code, thanks to Source Generator.
  • Commands: A convenient command system supporting up to four parameters with the [RelayCommand] attribute.
  • Observable Collections: Flexible collections (ObservableList, ObservableDictionary<TKey, TValue>, and more) with support for synchronization, filtering, and sorting.
  • StarterKit: Ready-to-use components, including virtualized lists, value converters, and support for DI frameworks (Zenject, VContainer).
  • Easy Debugging: View and modify ViewModel states directly in the Unity Inspector, with change logging and clear error messages.
  • Cross-Platform: Full support for all Unity platforms (PC, mobile, consoles).
  • High Performance: No reflection, minimal allocations, and no boxing/unboxing.

Example ViewModel Code

Here’s how easy it is to create a ViewModel with Aspid.MVVM:

[ViewModel]
public partial class PersonViewModel
{ 
    [BindAlso(nameof(FullName))]
    [Bind] private string _firstName;

    [BindAlso(nameof(FullName))]
    [Bind] private string _lastName;

    private string FullName => $"{_firstName} {_lastName}"; 

    public PersonViewModel(string firstName, string lastName)
    {
        _firstName = firstName;
        _lastName = lastName;
    }
}
[ViewModel]
public partial class PeopleViewModel
{
    [Bind] private ObservableList<PersonViewModel> _peopleList = new();

    [RelayCommand]
    private void AddPerson(string name)
    {
        string[] parts = name.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
        _peopleList.Add(new PersonViewModel(parts[0], parts[1]));
    }
}

Where to Find Aspid.MVVM?

  • Unity Asset Store: Aspid.MVVM (purchasing is a great way to support development).
  • GitHub: Free Version — fully open-source.
  • Documentation: Comprehensive guide available on GitBook.

Who is Aspid.MVVM For?

  • Unity developers looking to simplify UI development and avoid “spaghetti code.”
  • Teams aiming for parallel workflows between designers and programmers.
  • Projects prioritizing performance, scalability, and flexibility.

Stay tuned for more updates and upcoming features—some really exciting ones are on the way!

4 Likes

2025.10.08
Hi! I’m a little surprised that no one has posted anything yet. I’ll be the first :smiley:
When I was browsing the “New releases” category, it immediately caught my eye, because I feel like there is a lack of good and well-thought-out MVVM implementations that FIT Unity (don’t impose unusual solutions that cause more complications than they solve; I’m looking at DI frameworks here, I know the package supports them, but I don’t think it imposes them). Anyway, I bought the package as soon as I saw it.

I don’t have an opinion about it yet, because I’ve only just found the time to look through it, but the description, documentation, and folder structure after importing made a good first impression on me.

I mainly want to see how the package works in combination with UGUI, and more specifically with the “New UI Widgets” UI component package. I am currently using the “Data Binding for Unity” package, which is directly supported by New UI Widgets.

I was creating a ViewModel (in Data Binding for Unity it was called Context), and using the inspector I could attach references to various Models (app data and business logic), which were often in the form of other components.
Later, with the help of Data Bind components, I could link various UI elements to data and commands defined in ViewModel.

I didn’t have to define anything else except ViewModel. The Model was my entire application, and the View was UI components (Image, Text, Button) added from the inspector.

I’m curious to see how this whole process will look with Aspid.MVVM. :smiley:

What drove you to create this package? Do you use it in your projects? What was the main idea behind it, and what problem was it primarily intended to solve?

1 Like

tl;dr: Please get rid of the View class (or autogenerate it using SourceGenerator for a specific ViewModel) so that you can directly hook the UI component binders to the ViewModel.

Okay, so I read the documentation, checked out a few examples you prepared, and I have one big problem with the package architecture.

As I mentioned in my previous post, I am currently using the “Data Bind for Unity” package, and apart from the fact that its documentation is very limited and simply poor, it approaches the subject of binding differently than Aspid.MVVM. In my opinion, it’s better. What’s it all about?

Main problem: Aspid.MVVM unnecessarily divides the View layer in the MVVM model into two parts:

  • UI components (Image, Text, Button) arranged in the editor by the designer
  • the View class created by the programmer

In .NET MVVM, the View layer is defined in XAML (created manually or generated by various UI building block tools). In Unity, I would expect the View layer to be defined using the Unity Editor and adding the appropriate UI objects to the Canvas. In the case of Aspid, not only does the designer arrange UI elements in the editor, but they also have to develop the View class along with it, which often looks like a Cpp Header file for ViewModel (I mean that it often declares what is already defined in the ViewModel class).

Why can’t we just have a ViewModel that is visible to Binder components, instead of needing an additional View class? Additionally, it looks as if data binding (key in MVVM architecture) is between UI components ↔ View, rather than between View (which consists of UI components and is not a separate part of it) ↔ ViewModel.

For comparison, here’s how it currently looks in my project using “Data Bind for Unity”:

For example, to display a message on the UI:

  1. I define ViewModel (also known as Context)
[Serializable]
public class MessageContext : BindableContextBase
{
	[SerializeField] private TalkAction _talkAction;
	
	private string _messageId;
	public string MessageId
	{
		get => _messageId;
		private set => SetField(ref _messageId, value);
	}
	
	private string _nickname;
	public string Nickname
	{
		get => _nickname;
		private set => SetField(ref _nickname, value);
	}

	private bool _isPlayer;
	public bool IsPlayer
	{
		get => _isPlayer;
		private set => SetField(ref _isPlayer, value);
	}

	private string _message;
	public string Message
	{
		get => _message;
		private set => SetField(ref _message, value);
	}

	public MessageContext()
	{ }

	public MessageContext(TalkAction talkAction) : this()
	{
		_talkAction = talkAction;
	}

	public override void Initialize()
	{
		base.Initialize();
		
		MessageId = _talkAction.Id.ToString();
		Nickname = _talkAction.Talkable.Nickname;
		IsPlayer = _talkAction.Talkable is PlayerController;
		Message = _talkAction.Message;
		
		_talkAction.PropertyChanged += OnTalkActionPropertyChanged;
	}

	protected override void Dispose(bool disposing)
	{
		_talkAction.PropertyChanged -= OnTalkActionPropertyChanged;

		base.Dispose(disposing);
	}

	private void OnTalkActionPropertyChanged(object sender, PropertyChangedEventArgs e)
	{
		if (e.PropertyName == nameof(TalkAction.Message))
		{
			Message = _talkAction.Message;
		}
	}
}

public abstract class BindableContextBase : IBindableContext
{ define Initialize, Dispose, SetField, other helper methods }

public interface IBindableContext : INotifyPropertyChanged, IInitializable, IDisposable
{ }

TalkAction is my model for user messages. In my case, Message can change at runtime, so I listen for changes from the model, but “Id, Nickname, and IsPlayer” cannot, so I assign them only once in Initialize. Properties could easily be generated using Source Generator and an attribute, as you did with [Bind] attribute.

  1. I define ContextCreator, which is limited to an empty class (Source Generator would also help here, just like your [ViewModel] attribute could generate it):
public class MessageContextCreator : ContextCreatorBase<MessageContext>
{ }

public abstract partial class ContextCreatorBase : MonoBehaviour
{
	[GenerateProperty(Access.Public, Access.Private)]
	[SerializeField] private ContextHolder _contextHolder;

	protected virtual void Reset()
	{
		_contextHolder = GetComponent<ContextHolder>();
	}
}

// BASE class
public abstract class ContextCreatorBase<T> : ContextCreatorBase
	where T : BindableContextBase
{
	[SerializeReference] private T _context;

	private void Start()
	{
		_context.Initialize();
		ContextHolder.Context = _context;
	}

	private void OnDestroy()
	{
		_context.Dispose();
	}
}

ContextCreatorBase is a MonoBehavior component that creates Contexts (ViewModels) by adding it as a component to the inspector and passing it to ContextHolder. You don’t have to use it, and you could delegate the creation of ViewModel (Context) to another place, e.g., using DI tools or anywhere else. Creating it from the MonoBehavior/Inspector component is simply convenient for the designer.

ContextHolder is a universal component through which UI components are attached to a given context (ViewModel).

  1. I create the UI using UI components (Image, Text, Button, etc.). The parent of all these objects is the object where I placed ContextHolder component. Using binding components, I can connect to ContextHolder (or, more precisely, ViewModel). ContextHolder is only a dumb MonoBehavior class, which can be used as a Component to hold ViewModel which is a pure C# class and cannot be attached as a component to the Inspector.

  1. If you define a method in MessageContext, you can also call that method from a Button (Command pattern).

As you can see, the programmer defines what should be in the ViewModel (MessageContext), including the data and methods that can be called from the UI, as well as the models that require access. We can connect these models from the Inspector (MessageContextCreator exposes serialized variables) or even use DI.

ContextCreator (MessageContextCreator) is a component that creates context, but it could also be created from another place (Bootstraper, other Context or something else). The MessageContextCreator component could be generated from Source Generator if there is applied the [ViewModel] attribute on the MessageContext class.

Instead, the designer arranges the UI using objects and components, and when they want to connect Text, Button, or anything else, they use Binder components (TextMeshPro Text Setter, Button Click Command, etc.), which indirectly provide data/methods from ViewModel through the universal ContextHolder.

The programmer exposes data/methods, the designer just hooks them up!

However, Aspid.MVVM has one additional step that, in my opinion, makes things more difficult, namely the View class, in which you have to define MonoBinders. Even in typical .NET applications, you don’t have anything like that. You define the View using XAML and the ViewModel in another class. There is no View class that you MUST define, as in Aspid. Who should write such a class? A programmer or a designer? The programmer will actually repeat what they have already defined in the ViewModel class, only using the MonoBinder type and [RequireBinder]. The variables must also have exactly the same names in ViewModel and View, which is not conducive to later code refactoring:

If a programmer writes code (ViewModel and View?) and a designer arranges the UI in an editor, this approach is not convenient, but rather limiting for the designer, who will then want to connect to it:


You should not limit someone who is making UI to using only 1 binder per data (_inputText in this case).

Summary: In my opinion, View classes are unnecessary. The View layer in .NET is XAML, and in the context of Unity, it is UI components and their arrangement in the editor. The View layer should be accessible to the designer (hence the simple XAML markup language in .NET instead of defining the UI in C# code; or some sort of visual UI editors such as UGUI or UI Toolkit Builder). Furthermore, UI elements should be attached to the ViewModel, not to the View class. Okay, something has to hold a reference to the ViewModel, but there definitely shouldn’t be any extra code to make that possible. In the example I gave above, it is ContextHolder (universal for every ViewModel) that holds the ViewModel, and ContextCreator that creates a ViewModel.

—===—===—===—===—
tl;dr: Get rid of OneWay, TwoWay, OneTime, OneWayToSource binding attributes. Bind attribute should only expose data, not define how it needs to be linked to UI.

Another shorter, but equally serious problem:
ViewModels expose state and notifications. Views decides whether a given property is OneWay, TwoWay, OneTime, etc. ViewModel shouldn’t know how the UI will present or edit the data!

Bind attribute should only expose the data, not decide how the UI can be attached to it.

It took me some time to write this and explain the things. Please consider these changes. I am also open to discussion, because in general, the package looks good, but it has these flaws :grinning_face_with_smiling_eyes:

Thank you so much for supporting Aspid.MVVM—your purchase means a lot! We’re always thrilled with suggestions for simplifying and improving the framework, especially ones as detailed as yours. I’ll definitely break down all the questions step by step, but I’ll need some time to think it over—I’ll get back with a full response and examples as soon as possible. :blush:

1 Like

Take your time, don’t rush. I’m glad you’re considering my message. :blush: I like the package, it looks modern and well thought out, it works on Source Generators, and I’m just rooting for it to be improved, because it might replace my current toolset. However, with the ones I pointed out, I simply cannot accept it at the moment and introduce it to the projects I share with designers. I will wait for your message!

1 Like

A Brief Backstory

On April 19, 2024, we had a programmers’ meeting at work (about 10 programmers), where we discussed pressing issues. One of the topics was the significantly increased complexity of UI. None of the existing implementations fully met our needs:

  • Separation of logic from presentation.
  • Maximum performance (avoiding reflection and minimizing memory allocation, which is crucial for mobile games).
  • Parallel work between designers and programmers. (We needed to eliminate the programmer’s involvement in every minor UI change, while setting boundaries for changes to maintain better quality control.)

At that meeting, we didn’t make any decision on this topic. Nevertheless, that same evening, I made the first commit of a solution that later became a full-fledged, high-performance MVVM framework for Unity — Aspid.MVVM.


About Other Solutions That Were in the Project

To understand why Aspid.MVVM has certain components, I’ll tell you a bit more about the solutions we had before and what didn’t work for us.

MVP-Passive View

If we consider MVP-Passive View in its simplest form, it’s a View that contains serialized fields for UI components (Text, Image, etc.) and an UpdateView(some parameters) method that updates all these components. The Presenter handled the binding between View and Model.

This approach allows precise control over the View’s behavior by the programmer, but it limits designers even in minor UI changes. Other drawbacks include the need for a dedicated Presenter for each View and difficulties in creating nested Views. On the positive side, we could immediately understand what a given View represents through the View component in the Inspector.

Something Like Data Binding for Unity

We also had a solution in the project similar to what you mentioned (Data Binding for Unity). In its simple implementation, it’s a MonoBehaviour component that can be thought of as a ViewModel containing specific reactive fields. There were also Binding components (TextBinding and others) that could be linked, as in “Data Binding for Unity,” by specifying the ViewModel and the path to the required field.

We liked this approach because there was no need for programmer involvement in every minor UI change, as a technical designer could connect any number of Binding components to a specific reactive field. However, the problems with this approach were significant for us:

  1. Use of reflection for binding.
  2. Additional minor overhead: The Binding component contained a reference to the component (ViewModel) and the path to that field, which increased its size and meant that during refactoring of field names in the ViewModel, all Binding components became invalid for that field.
  3. It was impossible to get an idea of how it worked from the root object; you had to iterate through all child objects to understand it. Also, you couldn’t assess from the root object which Binding components had broken, if any. If the View is small, this isn’t critical, but if the View is large and complex, understanding how it works and finding errors became quite labor-intensive.

Among the pros, besides allowing UI changes without programmer intervention, we could also note easier work with nested ViewModels from code.

Hybrid Approach

Aspid.MVVM was developed with a focus on combining the best aspects of the two approaches above while maintaining high performance.


Why the View Class Exists

In the early implementations of the project, there was no View class. There was a ViewModel class inheriting from MonoBehaviour, and binders could be attached to its fields. This approach didn’t fully meet our requirements. In most cases on our project, ViewModels do not inherit from MonoBehaviour for certain reasons. At the same time, we had to keep our requirements in mind:

  1. We needed to link the ViewModel to the View with minimal overhead.
  2. Through the View component, we should immediately get an idea of what the given View represents.
  3. Easy debugging. Not always is the issue in the View. Often, the ViewModel processes data incorrectly, or the data that arrives isn’t what we expected. But sometimes it’s a designer error in setup. The debugging process needed to be maximally simplified.
  4. Some things, like position, rotation, or object visibility in certain Views, shouldn’t be set by designers because the programmer knows best how to implement them. And in many cases, we’d prefer designers not to have access to these settings at all.

How the View Component Solves All This:

  1. Starting with MonoBinder. Like in “Data Binding for Unity,” you specify the binder’s View and the ID of the field you want to link the target component to. But in reality, MonoBinder in the build doesn’t contain either the View field or the ID field. These fields exist only at the Editor level for setup convenience. All binding logic is encapsulated in the View component. All field IDs are generated using a static class and shared between all Views and ViewModels, which reduces memory allocation for IDs in binders and speeds up binder linking to the required field in the ViewModel (these are implementation specifics).
[View]
public partial class MyView : MonoView
{
    [RequireBinder(typeof(string))]
    [SerializeField] private MonoBinder[] _name;
}

// Generated
public partial class MyView : IView
{
    public void Initialize(IViewModel viewModel)
    {
        _name.BindSafely(viewModel.FindBindableMember(new(Ids.Name)));
    }

    // Other generated code
}
  1. Since we have a View component, we can immediately get an idea of how the View works through it. We can also see which binders have broken, if any.

  2. Through the View component, we can debug our representation by changing ViewModel data via the component if needed.

  3. We develop games. And many things depend on proper setup that only the programmer knows. For example, I wouldn’t want to allow a designer to set a Transform via the Inspector if its position should change, as they don’t know which Transform to put there.

[View]
public partial class MyView : MonoView
{
    // Disables GameObject based on the _isActive value in ViewModel.
    private GameObjectVisibleBinder _isActive => new(gameObject);
    // Sets the position of the Transform component based on the _position value in ViewModel.
    private TransformPositionBinder _position => new(transform);
}

All these functions provided by the View components were necessary for us. Especially when working with complex UIs that included child Views.

Universal View

Despite the fact that writing a separate View component is a bit routine, in our case it was necessary. Later, we started migrating simpler Views to Aspid.MVVM, where writing a separate View component is excessive. Currently, we’re working on Aspid.MVVM version 1.1.0, which will introduce a Universal View. I’ve attached a screenshot of the prototype below. It may not be exactly like this in the release version. You can track progress on the project’s GitHub page.


Linking ViewModel to View

Just like in “Data Binding for Unity,” there’s a separate component that offers linking the ViewModel using several possible ViewModel creation types.




Refactoring Issues

As you rightly noted, identical names in View and ViewModel complicate refactoring. But it turned out to be the lesser of two evils for us. I haven’t used “Data Binding for Unity,” but I think that when changing a property name in the ViewModel, the binding components lost their references there.

In Aspid.MVVM, there are ways to avoid losing the reference to the MonoBinder component when changing a property name in the ViewModel:
// Before

[ViewModel]
public partial class MyViewModel
{
    [Bind] private string _name;
}

[View]
public partial class MyView : MonoView
{
    [RequireBinder(typeof(string))]
    [SerializeField] private MonoBinder[] _name;
}

// After
[ViewModel]
public partial class MyViewModel
{
    // Renamed the field
    [Bind] private string _firstName;
}

[View]
public partial class MyView : MonoView
{
    // Using the BindId attribute, we can override the ID.
    // Thus, changing the name in the ViewModel won't affect the MonoBinder components.
    [BindId("FirstName")]
    [RequireBinder(typeof(string))]
    [SerializeField] private MonoBinder[] _name;
}

Design Features

  1. You mention limiting designers when we specify not an array of MonoBinders, but a single instance:
[View]
public partial class MyView : MonoView
{
    [RequireBinder(typeof(string))]
    [SerializeField] private MonoBinder _inputName;
}

In reality, this is true. This is just one of the framework’s options. You don’t have to do it. In our case, we sometimes use this feature specifically to prevent logic errors.

  1. Use of [OneWayBind] and other attributes: In most cases on our project, we use the [OneWayBind] attribute. This not only improves performance but also helps the programmer better understand the ViewModel’s operation and where data might come from. For observable collections, we most often use [OneTimeBind], as we expect changes inside the collection, not to the collection itself. And using [OneTimeBind] is one of the most efficient methods. Explicitly specifying [OneWayToSource] indicates that data comes strictly from the View and isn’t expected from other sources.
[ViewModel]
public partial class MyViewModel
{
    [OneWayToSourceBind] private int _someParameter;
    [OneTimeBind] private readonly ObservableList<IViewModel> _elements;
}
[View]
public partial class MyView : MonoView
{
    [SerializeField] private OneWayToSourceValue<int> _someParameter;
    [RequireBinder(typeof(IViewModel))]
    [SerializeField] private MonoBinder[] _elements;
}

Additional Integrations

  1. I hadn’t heard of New UI Widgets before, but it seems like a fairly popular solution. Possibly, in the future, there will be out-of-the-box integration support with Aspid.MVVM if additional binders are needed for integration.
  2. In the future, integration with UI Toolkit is also planned.

Summary

Thanks again for all your comments. In this section, I’ll briefly summarize the answers to your questions:

  1. General/Generated View — planned for the next version. You can track progress on the project’s GitHub page: GitHub.
  2. Integration with DI: VContainer/Zenject is not imposed. With integration, you get an additional option to link ViewModel to View via the ViewInitializer component.
  3. Design features: Possibly for most teams, additional binding attributes and other limitation options are excessive; nevertheless, since this approach turned out to be the most attractive for our team, perhaps others need these options too.
  4. What prompted the creation of the framework — a large, complex UI whose creation took a ton of time for both DEV and ART departments. In addition, it was necessary to maximize the parallelization of programmer and designer work.
  5. Is the framework used in your project — yes, several programmers and technical designers work with the framework daily.
  6. The main problem the framework solves — easing the creation of complex UIs that can include many nested representations, simplifying their debugging, and flexibility in UI changes. I’ll try to answer more details below in response to your questions.

Additional Links:

Documentation
Project on GitHub
GitHub Repository
Asset Store

Hi Everyone, Version 1.0.5 is live.

What’s Changed


Features

  • Add new text binders
  • Add new localization binders

Fixes

  • Fix RectTransformSetters

Improvements

  • Improve logs
  • Improve profiler analysis

Full Changelog : v1.0.4...v1.0.5

1 Like

Hey everyone!

It’s been a while since my last update, but I have great news for those following the development of Aspid.MVVM! :tada:

We’re actively working on the Universal View — one of the key features coming in version 1.1.0. You can track the progress here: Add general view by VPDPersonal · Pull Request #43 · VPDPersonal/Aspid.MVVM · GitHub

This isn’t the only improvement in the upcoming release! We’re also working on other important features.




Aspid.UnityFastTools

Another significant change — we’ve extracted common utility tools into a separate repository, Aspid.UnityFastTools

This will allow using these tools independently from the main framework and make the codebase more modular.


We’re doing everything possible to release the update in November! :rocket:

Thank you all for your support and interest in the project! I’d love to hear your feedback and suggestions.

1 Like

Finally I have some time to test the Aspid.MVVM and got one question. The doc states that binding the properties is not available. But could I somehow bind the property manually without code generator? Do you have some examples/reference doc what needs to be implemented in the model view so the view will “find” it?

Sorry for the delayed response - I only noticed your question today.


At the current moment, there is no possibility to combine manual binding with binding using the code generator. This capability was intentionally removed during the development stage due to the complex binding logic. In the near future (according to the plan, this year), the property binding capability will be added. Here is an example of the syntax:

// TwoWay
private bool IsOn1
{
    get => _isOn1;
    set => SetProperty(ref _isOn1, value);
}

// TwoWay
private bool IsOn2
{
    get => _isOn2;
    set
    {
        if (_isOn2 != value)
        {
            _isOn2 = value;
            OnPropertyChanged();
        }
    }
}

// Optimized options 
// Default TwoWay
[Bind]
private bool IsOn3
{
    get => _isOn3;
    set => SetIsOn3(ref _isOn3, value);
}

// Default TwoWay
[Bind]
private bool IsOn4
{
    get => _isOn4;
    set
    {
        if (_isOn4 != value)
        {
            _isOn4 = value;
            OnIsOn4Changed();
        }
    }
}

// Default OneTime
[Bind]
private bool IsOn5 => _isOn5;

// Default OneWayToSource
[Bind]
private bool IsOn6
{
    set => _isOn6 = value;
}

You can track the progress at Git Hub
Commits are added almost daily.

1 Like

Hi, This is my first time using the MVVM Architecture and sometime I’m a bit lost trying to use the Aspid plugin. It would be great if there was a documentation for the StarterKit that would describe a bit better how some of the binder work such as the ViewModelObservableListMonoBinder ect…

For example I’m stuck trying to replicate your ToDo example but instead of having an ObservableList I have an ObservableHashSet in my model data to ensure uniquity. The problem, there are no MonoBinder for the ObservableHashSet on which I can set a Factory with a prefab like is done in the ToDo example.

So I’ve been trying to make the ViewModelObservableListMonoBinder binder work with the ObservableHashSet.CreateSync method without success. It feel like I need to rewrite a completely new Binder that I would called ViewModelObservableCollectionMonoBinder and I guess would be heavily inspired from the ViewModelObservableListMonoBinder. But In your github you also mention in the feature for the observable collection the possibility of :

Easy synchronization between two dependent collections.

So I guess it is possible to sync my ObservableHashSet with an ObservableList that would then be attached to the ViewModelObservableListMonoBinder, but I can’t figure out how ?

Otherwise I learnt and I like the Aspid framework a lot, it allow for a clean architecture between the UI and the model. Thanks !

Hi!
Thanks for using Aspid.MVVM and sharing your feedback—it’s super helpful!
You’re spot on about the docs; they’re lacking, but we want to improve them in version 1.1.0. Same with binders—we’re adding more to the starter kit soon.
On CreateSync, it’d be awesome if it handled syncing between sets and lists like you want. We might add that later. Right now, it only works between similar collections. Check the example in the Aspid.Collections GitHub repo.
As a quick workaround, you could manually sync your ObservableHashSet to an ObservableList by listening for changes, then bind the list to ViewModelObservableListMonoBinder. Not perfect, but it works. Share more code if you need a snippet!

Glad you’re liking the framework!

Example from GitHub:

public class Todo
{
    // Some code.
}

public class TodoService
{
    private readonly ObservableList<Todo> _todos = new();

    public IReadOnlyObservableList<Todo> Todos => _todos;

    public void Add(Todo todo) =>
        _todos.Add(todo);

    public void Remove(Todo todo) =>
        _todos.Remove(todo);
}

public class TodoListViewModel : IDisposable
{
    private readonly TodoService _service;
    private readonly IReadOnlyObservableListSync<TodoItemViewModel> _items;
    
    public IReadOnlyObservableList<TodoItemViewModel> Items => _items;
    
    public TodoListViewModel(TodoService service)
    {
        _service = service;
        
        // Automatic Model -> ViewModel synchronization
        _items = _service.Todos.CreateSync(
            model => new TodoItemViewModel(model),
            isDisposable: true
        );
    }
    
    public void Dispose() => _items.Dispose();
}

public class TodoItemViewModel : IDisposable
{
    private Todo _model;

    public TodoItemViewModel(Todo model)
    {
        _model = model;
    }

    // Some code.
}

Thanks for the answer. Indeed the quickest workaround was to manually sync my ObservableHashSet to an ObservableList, I created an extension method to do that and it’s working like a charm.

For those that are interested here is how I did it :

One way synchronisation from ObservableHashSet to ObservableList
public static class ObservableHashSetExtension
{
    public static ObservableList<T> AttachList<T>(this ObservableHashSet<T> source) where T : notnull
    {
        if (source == null) throw new ArgumentNullException(nameof(source));

        var list = new ObservableList<T>(source);
        var weakHandler = new ObservableHashSetToObservableListUpdater<T>(source, list);
        source.CollectionChanged += weakHandler.OnCollectionChanged;

        return list;
    }

    private class ObservableHashSetToObservableListUpdater<T> where T : notnull
    {
        private readonly WeakReference<ObservableList<T>> _weakList;
        private readonly ObservableHashSet<T> _source;

        public ObservableHashSetToObservableListUpdater(ObservableHashSet<T> source, ObservableList<T> list)
        {
            _source = source;
            _weakList = new WeakReference<ObservableList<T>>(list);
        }

        public void OnCollectionChanged(INotifyCollectionChangedEventArgs<T> e)
        {
            if (!_weakList.TryGetTarget(out var list))
            {
                // The list has been garbage collected → unsubscribe and forget.
                _source.CollectionChanged -= OnCollectionChanged;
                return;
            }

            // Synchronize the list
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    if (e.IsSingleItem) list.Add(e.NewItem);
                    else foreach (T item in e.NewItems) list.Add(item);
                    break;

                case NotifyCollectionChangedAction.Remove:
                    if (e.IsSingleItem) list.Remove(e.OldItem);
                    else foreach (T item in e.OldItems) list.Remove(item);
                    break;

                case NotifyCollectionChangedAction.Reset:
                    list.Clear();
                    foreach (var item in _source)
                        list.Add(item);
                    break;
            }
        }
    }
}

On an other subject, I’m trying to Serialize and Deserialize an ObservableHashSet using JSON and I had issue during deserialization that was re-creating an instance of the ObservableHashSet and therefore was breaking event listening. More detail about it here.
It is suggested to implement the ICollection<T> Interface on the ObservableHashSet, so I mention it here just in case you think it would be a valuable modification.

1 Like

Hey there,

Still enjoying the framework a lot as I’m exploring more of the stuff that it can achieve. There is something I’m not sure to be doing correctly though. I would like to have an easy access to Unity Component from my ViewModel and the only way I found to do so was as following :

  1. Implement a custom MonoBinder for each ComponentType I need.
Code Example
[AddComponentContextMenu(typeof(Evo.UI.Tooltip), "Add EVO Tooltip Component Binder")]
[BindModeOverride(BindMode.OneWayToSource)]
public class EVOTooltipMonoBinder : ComponentMonoBinder<Evo.UI.Tooltip>, IReverseBinder<Evo.UI.Tooltip>
{
    public event Action<Evo.UI.Tooltip> ValueChanged;
    protected override void OnBound()
    {
        base.OnBound();
        ValueChanged?.Invoke(CachedComponent);
    }
}
  1. In the View I use a MonoBinder as usual.
Code Example
[View]
public partial class ExampleView : MonoView, IView<ExampleViewModel>
{
    [RequireBinder(typeof(Evo.UI.Tooltip))]
    [SerializeField] private MonoBinder[] _evoTooltip;
}
  1. In the ViewModel though, I can’t access the Binded Component in the ViewModel constructor as it is not set yet, I need to wait for the ValueChanged event to interact with the component
Code Example
[ViewModel]
public sealed partial class ExampleViewModel
{
    [OneWayToSourceBind] private Evo.UI.Tooltip _evoTooltip;
    public ExampleViewModel() { }//Here _evoTooltip is still null
    partial void OnEvoTooltipChanged(Tooltip newValue)
    {
        Debug.Log($"Evol Tooltip changed to {EvoTooltip}");
        if (EvoTooltip != null)
            EvoTooltip.description = "A new description set from the VM";
    }
}

It feels a bit hacky to do it this way, and it would be great If I could have a MonoBinder for any type of Component. At first I thought the ComponentMonoBinder<TComponent> could be used to do so, but it can’t be instantiated on a GameObject from the Editor. Ideally I guess (I tried so don’t really know how) I would need a Generic MonoBinder that instantiate the specific ComponentMonoBinder at runtime :

Code Example
public class GenericComponentBinder<TComponent> : ComponentMonoBinder<TComponent>, IReverseBinder<TComponent> where TComponent : Component
{
    public event Action<TComponent> ValueChanged;

    protected override void OnBound()
    {
        base.OnBound();
        Debug.LogWarning($"Binding with {CachedComponent}");
        ValueChanged?.Invoke(CachedComponent);
    }
}

[AddComponentContextMenu(typeof(Component), "Add General Component Binder")]
public class GenericComponentBinder : ComponentMonoBinder<Component>
{
    public void Awake()
    {
        /* 
         * 1) Get the type of the CachedComponent if possible ?
         * 2) Instantiate a GenericComponentBinder<TComponent> where TComponent match the Type of the CachedComponent
         * 3) Forward the Binding View and Id to the instantiated GenericComponentBinder<TComponent> (if type match)
         */
    }
}

EDIT
After reading the post of @IceTrooper1 I think I understand a bit better why the above example seems hacky. From what I understand, the architecture your are proposing is meant to bind individual field between the UI and the ViewModel. Let’s take the following example :

Let’s consider a ViewModel that need to have full control over a TextMeshPro UI component, so it will need to modify the Font, FontSize, Color, Alignement ect… From what I understand the way to do this using your architecture is to implement a MonoBinder Class for each field of the TextMeshPro (Your are already shipping most of those MonoBinder class for TextMeshPro) and then create the view, expose all of those binding and likewise in the ViewModel. This feel cumbersome… this add soo much boilerplate code and classes instead of simply binding the TextMeshPro instance directly with the ViewModel, where then the ViewModel can directly interact with the TextMeshPro instance…

Or is it not the case ? Could you provide an example on how a ViewModel could access every field of a TextMeshPro ?

Hey, glad you’re enjoying the framework! Let me address your questions:

1. Custom MonoBinder per component

Yes, in the current version you do need to implement a custom MonoBinder for each component you want to pass to the ViewModel. That said, you’re not limited to MonoBinder — you can also use a regular binder for this purpose:

[View]
public partial class ExampleView : MonoView, IView<ExampleViewModel>
{
    [SerializeField] private OneWayToSourceValue<Evo.UI.Tooltip> _evoTooltip;
}

2. Waiting for ValueChanged

Unfortunately, waiting for the ValueChanged event is indeed the correct approach right now. There are no plans to change this behavior in the current version.

3. What’s coming in the next version

We’ve made some improvements that should address your concerns:
We’ve added typed ComponentToSourceMonoBinder for many common components, for example:

Code Example
[AddBinderContextMenu(typeof(AudioSource))]
[AddComponentMenu("Aspid/MVVM/Binders/Audio/AudioSource/AudioSource To Source Binder")]
public sealed class AudioSourceToSourceMonoBinder : ComponentToSourceMonoBinder<AudioSource> { }

Additionally, a new IAnyReverseBinder interface has been introduced along with a general-purpose ComponentToSourceMonoBinder that can bind any component type:

Code Example
[AddComponentMenu("Aspid/MVVM/Binders/Components/Component To Source Binder")]
[AddBinderContextMenu(typeof(Component), Path = "Add General Binder/Component/Component To Source Binder")]
public sealed class ComponentToSourceMonoBinder : ComponentToSourceMonoBinder<Component>, IAnyReverseBinder
{
    public new event Action<object> ValueChanged;

    protected override void OnBound()
    {
        base.OnBound();
        ValueChanged?.Invoke(CachedComponent);
    }
}

Regarding [OneWayToSourceBind], the next version will also introduce property binding as an additional binding method, giving you more control over the setter logic:

Code Example
[OneWayToSourceBind]
private AudioSource Audio
{
    // A getter is optional

    set
    {
        // Your custom logic when the value is received from the View
    }
}

We’re getting close to the release and hope to ship it soon. If you’d like to try the new version right now, you can install it via UPM packages. For Unity 6 and above, first remove the existing Aspid folder from your Plugins directory, then import the following packages in order:

  1. https://github.com/VPDPersonal/Aspid.Internal.Unity.git
  2. https://github.com/VPDPersonal/Aspid.FastTools.git?path=Aspid.FastTools/Assets/Plugins/Aspid/FastTools#Develop
  3. https://github.com/VPDPersonal/Aspid.Collections.git
  4. https://github.com/VPDPersonal/Aspid.MVVM.git?path=Aspid.MVVM/Assets/Plugins/Aspid/MVVM#Version/Aspid.MVVM-1.1.0

Note: Aspid.MVVM now depends on a new package – Aspid.FastTools (Develop branch) — so make sure it’s installed first.

If you decide to try the pre-release version and run into any issues, please let us know and we’ll do our best to help!

1 Like

Perfect ! It took me a while to understand but thanks to your example it make much more sense now. I was only using MonoBinder in the View because I didn’t realized how non-MonoBinder were working. By defining my binding like so in the View :

[SerializeField] private OneWayToSourceValue<Evo.UI.Tooltip> _evoTooltip;

I can then simply drag and drop my Evo.UI.Tooltip component in the View instance on my Hierarchy ! This make so much more sense. Documentation / Tutorial are really unclear about that, took me a while to figure it out haha.

1 Like

Hey it’s me again, do not hesitate to tell me If I’m a bit annoying with all my question, the MVVM design pattern is very new to me and given the lack of documentation sometime I’m a bit lost and not sure the pattern I’m using are “good practice”.

I would like to go back to a simple example where the goal is for the ViewModel to have extensive control on a TextMeshPro component. In my previous post I found a way to access the Component from within the ViewModel but if I understood correctly this is not a good practice in MVVM design. In theory the ViewModel shouldn’t be aware of the Component use to build the View(i.e the TextMeshPro). Because it can lead to unexpected behavior (for example at some point I was calling a method on a View Component from the ViewModel but the View Component wasn’t even awaked yet…)

BUT In order for the ViewModel to be able to fully customize the TextMeshPro, it’s cumbersome to create a MonoBinder for each one of the TextMeshPro field (even though you already did it).

So, the real question then, is the following pattern acceptable and good practice ?

Code Sample
[View]
public partial class ExampleView : MonoView
{
    [SerializeField] TextMeshProUGUI _textComponent; // This is direcly attached through the Unity Inspector Window.

    //Those will be bind with the ViewModel
    private GenericOneWayBinder<string> _text;
    private GenericOneWayBinder<int> _fontSize;

    partial void OnInitializingInternal(IViewModel viewModel)
    {
        //The view take care of the binding from the ViewModel to the component field
        //Without having to create a custom MonoBinder for each field of the component we want to bind to.
        _text = new GenericOneWayBinder<string>(value =>
        {
            _textComponent.text = value;
        });

        _fontSize = new GenericOneWayBinder<int>((value) =>
        {
            _textComponent.fontSize = value;
        });
    }
}

[ViewModel]
public sealed partial class ExampleViewModel
{
    //Here the ViewModel doesn't care about the view component,
    //it doesn't care if the text and fontSize will be on a TextMeshPro or any other text rendering component
    [OneWayBind] private string _text;
    [OneWayBind] private int _fontSize;

    public ExampleViewModel() {
        Text = "Sample text";
        FontSize = 22;
    }
}

Hey, don’t hesitate to ask — these are good questions worth answering properly!

To put it simply: yes, it’s better not to pass the component into the ViewModel. Your example is good practice and closer to the intended spirit of MVVM than accessing the component directly from the ViewModel. The ViewModel correctly exposes only primitive values (string, int) with no knowledge of how they’ll be rendered — it doesn’t care whether it’s a TextMeshPro, a legacy Text, or anything else. The View takes full responsibility for mapping those values to component-specific calls. That’s the right separation of concerns.

Your example is correct, but it can be simplified slightly. The View can also bind properties using expression-body syntax with lazy caching:

[View]
public partial class ExampleView : MonoView
{
    [SerializeField] TextMeshProUGUI _textComponent;

    private GenericOneWayBinder<string> Text => new(value =>
    {
        _textComponent.text = value;
    });

    private GenericOneWayBinder<int> FontSize => new(value =>
    {
        _textComponent.fontSize = value;
    });
}

One important note: don’t call these properties directly — they are only intended to be used by the generated binding code.

You can also go a step further and group related fields into a struct, which lets you bind them all through a single binder:

public struct TextData
{
    public string Text;
    public int FontSize;
}

[ViewModel]
public sealed partial class ExampleViewModel
{
    [OneWayBind] private TextData _data;
}

[View]
public partial class ExampleView : MonoView
{
    [SerializeField] TextMeshProUGUI _textComponent;

    private GenericOneWayBinder<TextData> Data => new(value =>
    {
        _textComponent.text = value.Text;
        _textComponent.fontSize = value.FontSize;
    });
}
1 Like

Hey everyone,

We’re considering raising the minimum supported Unity version for Aspid.MVVM to Unity 6000.0. Supporting older LTS versions is starting to hold back improvements we’d like to ship.

Quick question for those still on older versions:

  • Which Unity version are you on, and what’s keeping you there?
  • Would this be a blocker, or are you okay staying on an older Aspid.MVVM release for that project?

If you prefer, you can also reply in the GitHub discussion here: Considering Unity 6 as the minimum supported version for Aspid.MVVM · VPDPersonal/Aspid.MVVM · Discussion #69 · GitHub

Older releases will stay available on GitHub, so nothing gets stranded — but if most of you have already moved to Unity 6, we’d rather focus our effort there.

Thanks!