Admins will be upgrading ORNL GitLab Servers on Saturday, 16 May 2026, from 7 AM until 11 AM EST. Repositories will experience intermittent outages during this time.
**2. `main.py` - Calling the Model (`src/nova_tutorial/main.py`):**
***Instantiate and Run**: In the `main()` function, we create an instance of `FractalViewModel` and call the `run_fractal_tool()` method, wrapped in a `try...except` block for basic error handling:
***Instantiate and Run**: In the `main()` function, we create an instance of `Fractal` and call the `run_fractal_tool()` method, wrapped in a `try...except` block for basic error handling:
@@ -47,8 +47,6 @@ The MVVM pattern consists of three core components:
The Model is agnostic to the UI. It doesn\'t know anything about how the data will be displayed or how the user will interact with it. It simply provides the data and the means to manipulate it.
*In the context of our NOVA tutorial, the Model will often include the logic for interacting with the NDIP platform via `nova-galaxy`.*
***View:** The View is the *user interface* (UI) of the application. It\'s responsible for:
* Displaying data to the user.
* Capturing user input (e.g., button clicks, text entered in a field, selections from a dropdown).
@@ -139,108 +137,96 @@ Finally, we connect a GUI element to the connector object. The template applicat
InputField(v_model="config.username")
```
## Implementing MVVM with `nova-mvvm` and Pydantic - Key Code Snippets
## Implementing MVVM with `nova-mvvm` and Pydantic
Let\'s see how to implement the MVVM pattern using `nova-mvvm` and incorporate Pydantic for data validation in our `FractalViewModel`. You can find the complete code for this episode in the `code/episode_4` directory. Here, we will focus on the key code snippets and explain the important parts.
Let\'s see how to implement the MVVM pattern using `nova-mvvm` and incorporate Pydantic for data validation.
**1. `FractalToolInput` Pydantic Model (`src/nova_tutorial/view_models/fractal_view_model.py`):**
**1. Adding Fractal to the ViewModel (`src/nova_tutorial/view_models/main.py`):**
***Defining the Model**: We start by defining a Pydantic model `FractalToolInput` to represent the input data for our fractal tool. This model uses type hints and `Literal` to enforce valid `fractal_type` values:
***Running our Model**: We start by adding a method to our ViewModel which will run the Fractal tool.
By defining `fractal_type` with `Literal[...]`, we ensure that only the specified string values are accepted, leveraging Pydantic\'s data validation capabilities.
**2. `FractalViewModel` Class (`src/nova_tutorial/view_models/fractal_view_model.py`):**
**2. Updating our Fractal Class for pydantaic and MVVM (`src/nova/tutorial/models/fractal.py**
***Imports**: The `FractalViewModel` now imports classes from `nova.mvvm.interface` and `nova.mvvm.trame_binding`, and `pydantic`:
***Adding new imports**: We need to add some imports for pydantic and working with base64 encondings to deal with the image.
```python
from nova.mvvm.interface import BindingInterface
from nova.mvvm.trame_binding import TrameBinding
from base64 import b64encode
from typing import Literal
from pydantic import BaseModel, Field
```
***`__init__` method**: In the `__init__` method, we now accept a `BindingInterface` instance (specifically, `TrameBinding`) as an argument. We also initialize state variables with leading underscores ( `_fractal_type`, `_run_button_disabled`, `_message`) to follow a convention for "internal" variables, and create bindings using `binding.new_bind(...)`:
***Update class variables:** Now we'll update fractal_type to support pydantic and add an image variable to store the image. Modify the variable declarations to the following:
```python
class FractalViewModel():
def __init__(self, binding: BindingInterface):
super().__init__()
self.fractal = Fractal()
self._fractal_type = "mandelbrot"
self._run_button_disabled = True
self._message = ""
self.run_button_disabled_bind = binding.new_bind(
linked_object=self,
linked_object_arguments=["run_button_disabled"],
)
self.message_bind = binding.new_bind(
linked_object=self,
linked_object_arguments=["message"],
)
self.fractal_type_bind = binding.new_bind(
linked_object=self,
linked_object_arguments=["fractal_type"]
)
```
The `binding.new_bind(...)` calls are crucial for setting up the MVVM pattern. They create `Communicator` objects that will manage the synchronization of state between the ViewModel and the View (UI).
***`set_fractal_type` method**: We modify `set_fractal_type` to use the `FractalToolInput` Pydantic model for validation. If validation fails, we update the `_message` state variable with the error:
***Decode the image data:** Finally, we need to decode the image that we receive as the output from the tool execution. Modify the section where we execute the tool to the following:
```python
def set_fractal_type(self, fractal_type: str):
try:
FractalToolInput(fractal_type=fractal_type) # Validate input using Pydantic
except ValidationError as e:
self._message = f"Validation Error: {e}"
self.message_bind.update_in_view(self._message) # Update message state
return
self._fractal_type = fractal_type
self.fractal_type_bind.update_in_view(self._fractal_type) # Update fractal_type state
Here, `FractalToolInput(fractal_type=fractal_type)` attempts to create an instance of the Pydantic model, which triggers validation. If `fractal_type` is invalid, a `ValidationError` is caught, and the error message is set in the ViewModel\'s `_message` state, which, thanks to binding, *will later* update the UI.
***`run_fractal_tool` method**: In `run_fractal_tool`, we now also use the `message_bind` and `run_button_disabled_bind` to update the UI state (even though we don\'t have a UI yet, this demonstrates good MVVM practice):
**3. Creating a FractalTab (`src/nova_tutorial/views/fractal_tab.py`):**
***Create a fractal tab**: Create a new file and add the following code:
Even without a View, we are already considering what the functionality that we\'ll need to support. We\'ve already created the bindings to server as our communicators between our future view and our new view model.
from trame.widgets import vuetify3 as vuetify
from nova.trame.view.components import InputField
from nova_tutorial.app.view_models.main import MainViewModel
class FractalTab:
**2. `main.py` - Wiring up TrameBinding (`src/nova_tutorial/main.py`):**
***Instantiate `TrameBinding` and Pass to ViewModel**: In `main()`, we now create a `TrameBinding` instance and pass it to the `FractalViewModel` constructor:
**4. Modify the tab panel (`src/nova_tutorial/views/tab_content_panel.py`):**
Add our new Fractal Tab to the tab content panel.
```python
def main():
server = get_server(None, client_type="vue3") # Trame server (not yet used for UI in this episode)
binding = TrameBinding(server.state)
fractal_vm = FractalViewModel(binding)
with vuetify.VWindow(v_model="active_tab"):
with vuetify.VWindowItem(value=1):
FractalTab(self.view_model) # Add FractalTab
with vuetify.VWindowItem(value=2):
SampleTab1()
with vuetify.VWindowItem(value=3):
SampleTab2()
```
This is the crucial step that "wires up" the ViewModel to the Trame binding, making it ready to interact with a Trame-based View in later episodes.