If you work with C#/.NET, you’ve almost certainly heard of, tried, or used both architectural patterns. But let’s be honest — many practicing developers aren’t too fond of the abstract language of design. We just take an existing template from colleagues, book authors, or AI, and start adding new methods, new variables, new properties.
But let’s also agree: sometimes it’s extremely useful to rise above and take a look at the abstract model — to examine that barely recognizable original shape of the project.
This is needed to grasp the essence of the core concepts, and often to “refactor” your creation — or, in more literary language, to make it more elegant and complete.
So, two architectural patterns used for similar yet different types of projects:
Model-View-Controller (MVC) and Model-View-ViewModel (MVVM)
Both contain a Model and a View.
Model — where the data lives. In more complex cases, it can be split into sublayers, but let’s not complicate it for now.
The model includes classes that represent data and methods that manipulate that data directly. For example, via Entity Framework, Dapper, or plain SqlClient calls. Or just in-memory data classes.
View — the visualization. What the user sees. Usually rather “dumb,” but beautiful.
Now, here’s the difference:
Model-View-Controller
The View displays data from the Controller. It usually doesn’t do much itself — it simply receives data from the controller (explained below).
Example:
@model List<Product>
<ul>
@foreach (var p in Model)
{
<li>@p.Name - $@p.Price</li>
}
</ul>
This shows how much each product costs.
Model-View-ViewModel
The View is bound to the ViewModel, typically via two-way data binding. This means user actions are passed down to the ViewModel, and the ViewModel can update the View’s elements in return.
Example:
<Entry Placeholder="Name" Text="{Binding NewName}" />
This displays data from the ViewModel, while
<Button Text="Add" Command="{Binding AddCommand}" />
sends user actions back to the ViewModel.
Now to the main actors: the Controller and the ViewModel.
Model-View-Controller
The Controller receives user input, manages the model, and prepares data for the view. It then triggers either a full (view.Render(model)) or partial (view.UpdateSection(data)) view update.
Example:
MVC: manual partial update, where the controller explicitly refreshes the label.
// ===== Model =====
class CounterModel {
public int Value { get; private set; }
public void Increment() => Value++;
}
// ===== View =====
class CounterView {
private Label _label;
private Button _button;
public event Action IncrementClicked;
public CounterView(Label label, Button button) {
_label = label;
_button = button;
_button.Click += (s, e) => IncrementClicked?.Invoke();
}
public void UpdateLabel(int value) {
_label.Text = $"Count: {value}";
}
}
// ===== Controller =====
class CounterController {
private CounterModel _model;
private CounterView _view;
public CounterController(CounterModel model, CounterView view) {
_model = model;
_view = view;
_view.IncrementClicked += OnIncrement;
_view.UpdateLabel(_model.Value); // initial display
}
private void OnIncrement() {
_model.Increment();
_view.UpdateLabel(_model.Value); // partial view update
}
}
Model-View-ViewModel
The ViewModel manages the state of the view (UI), holds its data, passes information to the model, and implements commands. Every change in the ViewModel is immediately reflected in the View.
Example:
MVVM: the ViewModel only changes the data, and the View reacts automatically via bindings.
// ===== ViewModel =====
class CounterViewModel : INotifyPropertyChanged {
private int _value;
public int Value {
get => _value;
set {
if (_value != value) {
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
public ICommand IncrementCommand { get; }
public CounterViewModel() {
IncrementCommand = new Command(() => Value++);
}
public event PropertyChangedEventHandler PropertyChanged;
}
<!-- ===== View (XAML, e.g. MAUI/WPF) ===== -->
<VerticalStackLayout>
<Label Text="{Binding Value}" />
<Button Text="Increment" Command="{Binding IncrementCommand}" />
</VerticalStackLayout>
// ===== Code-behind =====
public partial class CounterPage : ContentPage {
public CounterPage() {
InitializeComponent();
BindingContext = new CounterViewModel();
}
}
So why are there two different patterns?
The answer is simple:
The first case, Controller, is great for strong separation between a “smart server” and a “fast but simple” client-side display. That’s the typical setup when the server and UI are far apart and loosely connected — i.e., for web applications.
The second case, ViewModel, works best for transparent, straightforward separation of logic and presentation for each component, when there are “many channels of interaction” — i.e., for desktop or mobile applications.