Using Model-View-ViewModel pattern for UI development in 2025

In this blog post we will see how Model-View-ViewModel UI design pattern can be used with different JavaScript, TypeScript, and .NET Core UI frameworks that are used today in many projects.

MVVM design pattern consists of three connected layers, where topmost is View, that has connection with data binding to ViewModel, which handles Model preparation and needed UI tasks for presenting the data in UI. Models can be data bound to UI like ViewModels. Models are data and domain representations of the actual domain or data models like data entities, or just conceptual models that are connected in example to REST API controllers and/or repositories. In theory, model can contain more information than is stored in the single data entity.

For the current state of the view for MVVM usage, and available up-to-date frameworks, Microsoft 365 Copilot was used. Copilot provided information about currently available MVVM supporting JavaScript and TypeScript frameworks for UI development. M365 Copilot provided a list of available alternatives for JS and TS MVVM usage, which was compared against already outdated not very well supported Knockout.js framework, that is most pure implementation of MVVM supporting frameworks for JS and TS. Currently available and popular MVVM JS and TS frameworks included Vue.js, Angular, and React, of which React does not really support MVVM design pattern.

For .NET Core based UI projects, there are two examples for MVVM use from original Windows Presentation Foundation (WPF) MVVM Windows application framework to newer Blazor ASP.NET Core framework, which both can use the same ViewModels and Models, and create different View layer implementations for the different environments, web and desktop app. Both .NET Core UI framework implementations support data binding to ViewModels and Models.

Model-View-ViewModel pattern

Model-View-ViewModel pattern was introduced for Windows Presentation Foundation (WPF) use in .NET and was also designed in 2005 for HTML use. In this design pattern the UI design is differentiated from program code as XAML or HTML layer, where UI design can be changed without changing the ViewModel or Model code.

From earlier experience, this kind of design is very useful when developer or designer wants to create different views of the same ViewModel and Models, like product list view, product overlay view, and product details view. All these views can use same ViewModel and Model implementations, and view can be totally different and contain all or part of the commands created for domain logic.

MVVM pattern uses commands to execute domain commands that are not directly attached to UI. Commands can have parameters, that are used to filter data from ViewModel state and make changes to data storage or do other kind of domain logic functionalities. Command pattern also makes it possible to use Command Query Responsibility Segregation (CQRS) design pattern that differentiates command execution and queries by using concepts like Write model and Read model, which both can reside in different service interfaces. Also, Read model can be seperated data store from write store when using Event-driven architecture like Azure Event Hub event data streaming service.

MVVM also uses data-binding that can be done with one way, one way to source, or two-way data-binding modes, where all changes in example to product name field, are updated to other places and views that have data-binding to the same Model. Different frameworks support different modes of data-binding.

JavaScript and TypeScript MVVM support in detail

When asking guidance from M365 Copilot, Copilot provided following diagram about MVVM usage for different JS and TS UI frameworks.

MVVM JS and TS frameworks diagram

Knockout.js and Vue.js provide possibility to create ViewModels, Views, and Models for MVVM style based application. Angular has components model, which is replacing ViewModel, but works similarly.

Below is a table that was provided by GitHub Copilot, that describes the MVVM support for different UI frameworks.

Framework MVVM Support Data Binding Notes
Knockout.js Yes Two-way Pure MVVM, less maintained
Vue.js Partial Two-way Uses ViewModel-like pattern
Angular Partial One/two-way Uses components, not pure MVVM
React No One-way Follows MV* patterns

Table shows that Knockout.js that has been used more in earlier years, and that was included in the project templates in web projects for .NET, is not maintained so well anymore. Second and third options, Vue.js is a framework that can be used to create MVVM applications client side in TypeScript or JavaScript.

Knockout.js, Vue.js and Angular provide possibility to use Two-way data-binding. Because React is not MVVM supporting framework, this the usage of React is not described more in this blog post.

.NET Core MVVM support in detail

Below GitHub Copilot has created diagram that describes two .NET Core based case examples with WPF Windows application and Blazor Web application that are using MVVM pattern.

MVVM Architecture Diagram

In past projects, there have been successful experiences about creating two user interface projects with same ViewModel library, like it is shown in the above diagram. This means that same business logic, and same UI logic can be used in Windows application and in Web applications with Blazor and WPF.

Framework / UI Type MVVM Support Data Binding Shared ViewModel/Model Support Notes
WPF Yes One-way, Two-way Yes Native MVVM, strong XAML binding
Blazor Yes One-way, Two-way Yes MVVM by convention, supports shared logic

Both UI frameworks for .NET Core support MVVM design style project fully. In this example case it is also possible to use both for same business case, if native workstation application and web application both are needed, in example in transition phase.

Experimentation code examples

For this blog post M365 Copilot provided code examples for JavaScript UI frameworks, for View, ViewModel, and Model layers, and for different pages in the application.

Knockout.js (Outdated pure MVVM example)

Below we can see product UI example code that was created for Knockout.js by M365 Copilot.

View (pages and single overlay view)

<!DOCTYPE html>
<html>
<head>
  <title>Knockout MVVM Example</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-min.js"></script>
</head>
<body>
<div data-bind="if: currentView() === 'list'">
  <h1>Product Catalog</h1>
  <div class="product-grid" data-bind="foreach: products">
    <div class="product-card">
      <h3 data-bind="text: name"></h3>
      <p>Price: <span data-bind="text: price"></span></p>
      <p>Discounted: <span data-bind="text: discountedPrice"></span></p>
      <button data-bind="click: $parent.showOverlay">Quick View</button>
      <button data-bind="click: $parent.goToProductPage">View Page</button>
    </div>
  </div>
  <div class="overlay" data-bind="visible: selectedProduct">
    <div>
      <h2 data-bind="text: selectedProduct().name"></h2>
      <p data-bind="text: selectedProduct().description"></p>
      <p>Original Price: <span data-bind="text: selectedProduct().price"></span></p>
      <p>Discounted Price: <span data-bind="text: selectedProduct().discountedPrice"></span></p>
      <button data-bind="click: closeOverlay">Close</button>
    </div>
  </div>
</div>
<div data-bind="if: currentView() === 'product'">
  <h1 data-bind="text: selectedProduct().name"></h1>
  <p data-bind="text: selectedProduct().description"></p>
  <p>Price: <span data-bind="text: selectedProduct().price"></span></p>
  <p>Discounted: <span data-bind="text: selectedProduct().discountedPrice"></span></p>
  <button data-bind="click: goBack">Back to List</button>
</div>
<script src="app.js"></script>
</body>
</html>

In this example, there is single page application MVVM View HTML code for Knockout.js application. This code contains 3 different Views Product list page, Product details page, and product overlay page.

ViewModel and Model

function Product(name, price, description) {
  var self = this;
  self.name = name;
  self.price = ko.observable(price);
  self.description = description;
  self.discountedPrice = ko.computed(function() {
    let numericPrice = parseFloat(self.price().replace('$', ''));
    return '$' + (numericPrice * 0.9).toFixed(2);
  });
}

function ProductListViewModel() {
  var self = this;
  self.products = ko.observableArray([
    new Product("Laptop", "$1200", "High-end laptop"),
    new Product("Phone", "$800", "Latest smartphone"),
    new Product("Tablet", "$500", "Portable tablet")
  ]);

  self.selectedProduct = ko.observable(null);
  self.currentView = ko.observable('list');

  self.showOverlay = function(product) { self.selectedProduct(product); };
  self.closeOverlay = function() { self.selectedProduct(null); };
  self.goToProductPage = function(product) { self.selectedProduct(product); self.currentView('product'); };
  self.goBack = function() { self.currentView('list'); self.selectedProduct(null); };
}

ko.applyBindings(new ProductListViewModel());

In the above example of the Knockout.js MVVM Product list ViewModel and Product Model, there is M365 Copilot generated version of the design pattern implementation. This example uses same Product Model for all the three views, product list rows, product details page, and for product overlay page for quick views from product list rows to details.

Vue.js (Up to date MVVM example)

Below we can see product UI example code that was created for Vue.js by M365 Copilot.

View (pages and single overlay view)

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Vue Example</title>
    <style>
      /* minimal styles to keep layout from breaking */
      .product-grid { display: flex; gap: 12px; }
      .product-card { border: 1px solid #ccc; padding: 12px; width: 200px; }
      .overlay { position: fixed; top: 20%; left: 20%; background: white; padding: 20px; border: 1px solid #333; }
    </style>
  </head>
  <body>
    <!-- view layer: mount target and router view -->
    <div id="app">
      <router-view></router-view>
    </div>

    <!-- Move templates into the HTML view layer -->
    <template id="product-list-template">
      <div>
        <h1>Product Catalog</h1>
        <div class="product-grid">
          <div class="product-card" v-for="p in products" :key="p.name">
            <h3></h3>
            <p>Price: $</p>
            <p>Discounted: $</p>
            <button @click="showOverlay(p)">Quick View</button>
            <button @click="goToProductPage(p)">View Page</button>
          </div>
        </div>
        <div v-if="selectedProduct" class="overlay">
          <h2></h2>
          <p></p>
          <p>Original Price: $</p>
          <p>Discounted Price: $</p>
          <button @click="closeOverlay">Close</button>
        </div>
      </div>
    </template>

    <template id="product-page-template">
      <div>
        <h1></h1>
        <p></p>
        <p>Price: $</p>
        <p>Discounted: $</p>
        <button @click="$router.push('/')">Back to List</button>
      </div>
    </template>

    <!-- include Vue + VueRouter from CDN (or use your local builds) -->
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
    <script src="./app.js"></script>
  </body>
</html>

In this example, there is single page application MVVM View HTML code for Vue.js application. This code contains 3 different Views Product list page, Product details page, and product overlay page.

ViewModel and Model

// ...existing code...
const Product = (name, price, description) => ({ name, price, description });
const products = [
  Product("Laptop", 1200, "High-end laptop"),
  Product("Phone", 800, "Latest smartphone"),
  Product("Tablet", 500, "Portable tablet")
];

const ProductList = {
  template: '#product-list-template',
  data() { return { products, selectedProduct: null }; },
  methods: {
    showOverlay(p) { this.selectedProduct = p; },
    closeOverlay() { this.selectedProduct = null; },
    goToProductPage(p) { this.$router.push({ name: 'product', params: { name: p.name } }); }
  }
};

const ProductPage = {
  template: '#product-page-template',
  computed: {
    product() { return products.find(p => p.name === this.$route.params.name); }
  }
};

const routes = [
  { path: '/', component: ProductList },
  { path: '/product/:name', name: 'product', component: ProductPage }
];

const router = VueRouter.createRouter({ history: VueRouter.createWebHashHistory(), routes });
const app = Vue.createApp({});
app.use(router);
app.mount('#app');

In the above example of the Vue.js MVVM Product list and ProductPage ViewModels and Product Model, is M365 Copilot generated version of the design pattern implementation. This example uses same Product Model for all the three views, product list rows, product details page, and for product overlay page for quick views from product list rows to details. In this M365 Copilot generated version for Vue.js, the pages have their own dedicated ViewModels, which can reuse the same Model implementation, and could also use same product ViewModel implementation for product specific UI functionalities.

React (not MVVM example)

Below we can see product UI example code that was created for React by M365 Copilot. This example is not following the MVVM design that was shown for Vue.js and for Knockout.js frameworks before. This example can be used to see differences in the UI implementation between MVVM based design and React design.

View (Single page application HTML code)

<!DOCTYPE html>
<html>
<head>
  <title>React MVVM Example</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/react-router-dom@6/umd/react-router-dom.development.js"></script>
  <style>
body { font-family: Arial, sans-serif; margin: 20px; }
.product-grid { display: flex; gap: 10px; flex-wrap: wrap; }
.product-card { border: 1px solid #ccc; padding: 10px; width: 150px; }
.overlay { position: fixed; top: 20%; left: 30%; background: #fff; border: 1px solid #333; padding: 20px; }
button { margin-top: 10px; }
</style>
</head>
<body>
<div id="root"></div>
<script src="app.js" type="text/babel"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</body>
</html>

In this example, there is single page application View HTML code for React application. In React, this HTML page does not contain all of the MVVM design based views, like Vue.js and Knowckout.js examples did.

View, ViewModel, and Model

const { BrowserRouter, Routes, Route, useNavigate, useParams } = ReactRouterDOM;

const products = [
  { name: "Laptop", price: 1200, description: "High-end laptop" },
  { name: "Phone", price: 800, description: "Latest smartphone" },
  { name: "Tablet", price: 500, description: "Portable tablet" }
];

function ProductList() {
  const navigate = useNavigate();
  const [selectedProduct, setSelectedProduct] = React.useState(null);

  return (
    <div>
      <h1>Product Catalog</h1>
      <div className="product-grid">
        {products.map(p => (
          <div key={p.name} className="product-card">
            <h3>{p.name}</h3>
            <p>Price: ${p.price}</p>
            <p>Discounted: ${(p.price * 0.9).toFixed(2)}</p>
            <button onClick={() => setSelectedProduct(p)}>Quick View</button>
            <button onClick={() => navigate(`/product/${p.name}`)}>View Page</button>
          </div>
        ))}
      </div>
      {selectedProduct && (
        <div className="overlay">
          <h2>{selectedProduct.name}</h2>
          <p>{selectedProduct.description}</p>
          <p>Original Price: ${selectedProduct.price}</p>
          <p>Discounted Price: ${(selectedProduct.price * 0.9).toFixed(2)}</p>
          <button onClick={() => setSelectedProduct(null)}>Close</button>
        </div>
      )}
    </div>
  );
}

function ProductPage() {
  const { name } = useParams();
  const navigate = useNavigate();
  const product = products.find(p => p.name === name);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>Price: ${product.price}</p>
      <p>Discounted: ${(product.price * 0.9).toFixed(2)}</p>
      <button onClick={() => navigate('/')}>Back to List</button>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<ProductList />} />
        <Route path="/product/:name" element={<ProductPage />} />
      </Routes>
    </BrowserRouter>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

In this React application example Product list and Product details pages are separated as own pages with functional design pattern. Overlay product view is included in the product list page function code, and not separated from as its own template here, when M365 Copilot was the one that generated this example. There can be ways to detach the overlay view from the page view, to its own function, but for this blog post, React is not the main concern, because MVVM support is not available in React applications.

Conclusion

When using MVVM design pattern in 2025 it is still possible to use up-to-date UI frameworks for development and create multi-UI applications with same business logic and UI logic layers.

When separating XAML markup (WPF), and HTML and CSS (JS and TS) there are many possibilities for using already existing UI logic and business logic, and creating updated UI designs, without a need for updating the code that is critical for the business.

When comparing these three code examples, the MVVM based applications and ViewModels and Models of these examples need no changes when updating the HTML or CSS for the UI, and if UI logic does not change, this MVVM based approach can provide possibility to do UI design changes without JavaScript or TypeScript coding skills.

Based on experiences from many projects where MVVM design pattern has been used, it has been easy way for maintaining the UI logic and business logic behind the actual UI markup code. Also, different assemblies can be used to extract the ViewModel and Model logic from the View layer in .NET Core project level. This also makes it possible to create new UI View layers for old applications, or creating different View layer projects for different platforms, like MacOS or other operating systems etc.