📖 Uvod
O projektu
Ta projekt zajema implementacijo dveh sodobnih .NET aplikacij:
- MAUI Aplikacija - Cross-platform mobilna/namizna aplikacija za upravljanje objav (Posts)
- Blazor Aplikacija - Spletna aplikacija za upravljanje produktov s podatkovnimi vizualizacijami
Obe aplikaciji uporabljata REST API zaledja za izvajanje vseh CRUD operacij (Create, Read, Update, Delete).
Uporabljene tehnologije
.NET 9.0
Najnovejša verzija .NET platforme
C# 12
Programski jezik za celotno aplikacijo
XAML
Deklarativna UI sintaksa za MAUI
Razor
Komponente za Blazor UI
Chart.js
Knjižnica za vizualizacijo podatkov
Bootstrap 5
CSS framework za odzivno oblikovanje
📱 Naloga 7 - .NET MAUI Aplikacija
Pregled aplikacije
MAUI (Multi-platform App UI) aplikacija je cross-platform rešitev za upravljanje objav preko REST API-ja
https://jsonplaceholder.typicode.com/posts. Aplikacija omogoča:
- ✅ Prikaz seznama objav (Posts)
- ✅ Dodajanje novih objav
- ✅ Urejanje obstoječih objav
- ✅ Brisanje objav
- ✅ Iskanje po naslovu
- ✅ Sortiranje (A-Z, Z-A)
- ✅ Prikaz statusnih sporočil
Arhitektura projekta
MAUI/
├── MainPage.xaml # Glavna stran z seznamom objav
├── MainPage.xaml.cs # Code-behind za glavno stran
├── Models/
│ └── Post.cs # Model entitete Post
├── Services/
│ └── ApiService.cs # Servis za komunikacijo z API
├── Pages/
│ ├── EditPage.xaml # Stran za dodajanje/urejanje
│ ├── EditPage.xaml.cs
│ └── AboutPage.xaml # Informacijska stran
├── Components/
│ ├── StatusMessage.xaml # Custom komponenta za obvestila
│ └── StatusMessage.xaml.cs
└── MauiProgram.cs # Konfiguracija aplikacije
1. Model podatkov - Post.cs
Definicija entitete za objave:
public class Post
{
public int Id { get; set; }
public int UserId { get; set; }
public string Title { get; set; }
public string Body { get; set; }
}
Razlaga: Preprost POCO (Plain Old CLR Object) razred, ki predstavlja objavo. Vsebuje vse potrebne lastnosti za serializacijo/deserializacijo JSON podatkov iz API-ja.
2. API Servis - ApiService.cs
Centralizirana komunikacija z REST API:
🔹 GET - Pridobivanje vseh objav
public async Task<List<Post>> GetPostsAsync()
{
var response = await _client.GetStringAsync(Url);
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<List<Post>>(response, options);
}
Primer uporabe: Klic await _apiService.GetPostsAsync() vrne seznam prvih 20 objav.
Uporablja PropertyNameCaseInsensitive za robustno deserializacijo ne glede na velikost črk v JSON-u.
🔹 POST - Dodajanje nove objave
public async Task<bool> AddPostAsync(Post post)
{
var json = JsonSerializer.Serialize(post);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync(Url, content);
return response.IsSuccessStatusCode;
}
Primer: Serializira objekt v JSON in ga pošlje na API. Vrne true če je HTTP status 2xx (uspeh).
🔹 PUT - Posodobitev objave
public async Task<bool> UpdatePostAsync(Post post)
{
var json = JsonSerializer.Serialize(post);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PutAsync($"{Url}/{post.Id}", content);
return response.IsSuccessStatusCode;
}
🔹 DELETE - Brisanje objave
public async Task<bool> DeletePostAsync(int id)
{
var response = await _client.DeleteAsync($"{Url}/{id}");
return response.IsSuccessStatusCode;
}
Opomba: Vsi методи se obnaša asinhronno (async/await), kar omogoča
ne-blokirajočo UI izvršitev.
3. Glavna stran - MainPage.xaml
🎨 Layout struktura
<Grid RowDefinitions="Auto, *, Auto, Auto" Padding="10" RowSpacing="10">
<!-- Row 0: StatusMessage + Iskanje + Sortiranje -->
<!-- Row 1: ListView z objavami -->
<!-- Row 2: Gumb "Dodaj novo objavo" -->
<!-- Row 3: Gumb "O aplikaciji" -->
</Grid>
Uporablja Grid Layout s 4 vrsticami. Prva vrstica je Auto (se prilagodi vsebini),
druga ima * (zasede preostali prostor), ostale dve sta Auto.
🔍 Iskanje in sortiranje
<Entry Placeholder="Išči po naslovu..."
TextChanged="OnSearchChanged" />
<Picker Title="Sortiraj"
SelectedIndexChanged="OnSortChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>A-Z</x:String>
<x:String>Z-A</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
Eventi:
TextChanged- sproži se ob vsaki spremembi besedilaSelectedIndexChanged- sproži se ob izbiri opcije
📋 ListView z objavami
<ListView x:Name="PostsListView"
ItemSelected="OnPostSelected"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Text="Izbriši"
IsDestructive="True"
Clicked="OnDeleteClicked"
CommandParameter="{Binding .}"/>
</ViewCell.ContextActions>
<StackLayout Padding="10">
<Label Text="{Binding Title}" FontAttributes="Bold"/>
<Label Text="{Binding Body}" TextColor="Gray"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Ključne funkcionalnosti:
ItemSelectedevent - odpre EditPage za urejanjeContextActions- swipe za brisanje (iOS/Android pattern)CommandParameter="{Binding .}"- poda celoten Post objektHasUnevenRows- omogoča dinamično višino celic
4. Code-behind logika - MainPage.xaml.cs
🔄 Lifecycle & Inicializacija
public ObservableCollection<Post> Posts { get; set; } = new();
private List<Post> _allPosts = new();
protected override async void OnAppearing()
{
base.OnAppearing();
if(Posts.Count == 0)
{
await LoadData();
}
}
ObservableCollection avtomatsko osvežuje UI ob spremembah (Add, Remove).
OnAppearing() se kliče vsakič, ko se stran prikaže - tukaj preverimo če so podatki že naloženi.
🔍 Implementacija iskanja
private void OnSearchChanged(object sender, TextChangedEventArgs e)
{
var term = e.NewTextValue.ToLower();
if (string.IsNullOrWhiteSpace(term))
{
RefreshList(_allPosts);
}
else
{
var filtered = _allPosts
.Where(p => p.Title.ToLower().Contains(term))
.ToList();
RefreshList(filtered);
}
}
Pristop: Filtriranje se izvaja lokalno na že naloženih podatkih (_allPosts).
Uporablja case-insensitive iskanje (ToLower()). Če je iskalni niz prazen, prikaže vse.
🔀 Sortiranje
private void OnSortChanged(object sender, EventArgs e)
{
var picker = (Picker)sender;
int selectedIndex = picker.SelectedIndex;
var sorted = selectedIndex == 0
? Posts.OrderBy(p => p.Title).ToList()
: Posts.OrderByDescending(p => p.Title).ToList();
RefreshList(sorted);
}
🗑️ Brisanje z uporabniško potrditev
private async void OnDeleteClicked(object sender, EventArgs e)
{
var menuItem = (MenuItem)sender;
var post = (Post)menuItem.CommandParameter;
bool success = await _apiService.DeletePostAsync(post.Id);
if(success)
{
Posts.Remove(post);
_allPosts.Remove(post);
MyStatusMessage.Show("Uspešno izbrisano!", false);
}
else
{
MyStatusMessage.Show("Napaka pri brisanju!", true);
}
}
Flow:
1. Pridobi Post iz CommandParameter
2. Pošlje DELETE zahtevo na API
3. Če uspešno, odstrani iz obeh kolekcij (Posts in _allPosts)
4. Prikaže statusno sporočilo preko custom komponente
➕ Dodajanje/Urejanje
private async void OnAddClicked(object sender, EventArgs e)
{
var page = new EditPage(null);
await Navigation.PushAsync(page);
var newPost = await page._completion.Task;
if (newPost != null)
{
bool success = await _apiService.AddPostAsync(newPost);
if (success)
{
newPost.Id = _allPosts.Max(p => p.Id) + 1;
_allPosts.Add(newPost);
Posts.Add(newPost);
MyStatusMessage.Show("Uspešno dodano!", false);
}
}
}
private async void OnPostSelected(object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem == null) return;
var post = (Post)e.SelectedItem;
var page = new EditPage(post);
await Navigation.PushAsync(page);
var updatedPost = await page._completion.Task;
if (updatedPost != null)
{
bool success = await _apiService.UpdatePostAsync(updatedPost);
if (success)
{
post.Title = updatedPost.Title;
post.Body = updatedPost.Body;
RefreshList(_allPosts);
MyStatusMessage.Show("Uspešno posodobljeno!", false);
}
}
PostsListView.SelectedItem = null;
}
TaskCompletionSource vzorec: EditPage uporablja TaskCompletionSource,
kar omogoča "čakanje" na rezultat iz navigirane strani. Ko uporabnik klikne "Shrani" ali "Prekliči",
se Task zaključi in glavna stran prejme rezultat.
5. EditPage - Dodajanje/Urejanje objav
📝 XAML UI
<VerticalStackLayout Spacing="15" Padding="20">
<Label Text="Naslov:" />
<Entry x:Name="EntryTitle"
Placeholder="Vnesi naslov"
TextChanged="OnTextChanged"/>
<Label Text="Vsebina:" />
<Entry x:Name="EntryBody"
Placeholder="Vnesi vsebino"
HeightRequest="150"
TextChanged="OnTextChanged"/>
<Label x:Name="ErrorLabel"
TextColor="Red"
IsVisible="False"/>
<Button Text="Shrani" Clicked="OnSaveClicked"/>
<Button Text="Prekliči" Clicked="OnCancelClicked"/>
</VerticalStackLayout>
✅ Validacija v realnem času
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.NewTextValue))
{
ErrorLabel.Text = "Polje ne sme biti prazno!";
ErrorLabel.IsVisible = true;
}
else
{
ErrorLabel.IsVisible = false;
}
}
Validacija se izvaja ob vsaki spremembi besedila (TextChanged event).
Prikaže opozorilo, če je polje prazno.
💾 Shranjevanje
private async void OnSaveClicked(object sender, EventArgs e)
{
if(string.IsNullOrWhiteSpace(EntryTitle.Text) ||
string.IsNullOrWhiteSpace(EntryBody.Text))
{
await DisplayAlert("Napaka", "Vsa polja morajo biti izpolnjena!", "OK");
return;
}
_post.Title = EntryTitle.Text;
_post.Body = EntryBody.Text;
_completion.SetResult(_post);
await Navigation.PopAsync();
}
_completion.SetResult(_post) posreduje rezultat nazaj na MainPage.
Navigation.PopAsync() zapre trenutno stran in se vrne na prejšnjo.
6. StatusMessage - Custom komponenta
🎨 XAML definicija
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
x:Class="MAUI.Components.StatusMessage"
IsVisible="False">
<Border StrokeShape="RoundRectangle 10" Padding="10">
<Label x:Name="MessageLabel"
TextColor="White"
HorizontalOptions="Center"
VerticalOptions="Center"
FontAttributes="Bold"/>
</Border>
</ContentView>
⏱️ Časovno omejena vidnost
public async void Show(string message, bool isError)
{
MessageLabel.Text = message;
MessageBorder.BackgroundColor = isError ? Colors.Red : Colors.Green;
this.IsVisible = true;
await Task.Delay(3000);
this.IsVisible = false;
}
Funkcionalnost: Prikaže sporočilo za 3 sekunde, nato se avtomatsko skrije. Barva se dinamično nastavi glede na tip sporočila (rdeča za napako, zelena za uspeh).
✅ Izpolnjene zahteve - MAUI
| Zahteva | Implementacija | Lokacija |
|---|---|---|
| Pridobivanje podatkov iz API-ja | ✅ ApiService.GetPostsAsync() | Services/ApiService.cs |
| Vse CRUD operacije | ✅ GET, POST, PUT, DELETE | Services/ApiService.cs |
| Preverjanje veljavnosti podatkov | ✅ TextChanged validacija, DisplayAlert | EditPage.xaml.cs |
| Prikaz sporočil o napakah | ✅ StatusMessage komponenta | Components/StatusMessage.xaml |
| Vsaj 1 custom komponenta | ✅ StatusMessage | Components/ |
| Vsaj 3 različne postavitve | ✅ Grid, StackLayout, VerticalStackLayout | *.xaml files |
| Vsaj 5 različnih tipov dogodkov | ✅ TextChanged, Clicked, SelectedIndexChanged, ItemSelected, ContextActions | MainPage.xaml, EditPage.xaml |
| Vsaj 2 podstrani | ✅ EditPage, AboutPage | Pages/ |
| Sortiranje po atributu | ✅ Sortiranje A-Z, Z-A po Title | MainPage.xaml.cs |
| Iskanje po atributu | ✅ Iskanje po Title | MainPage.xaml.cs |
| Pravilno obravnavanje napak | ✅ try-catch, status codes, uporabniška obvestila | MainPage.xaml.cs |
🎯 Ključne funkcionalnosti
📱 Cross-platform
Deluje na Windows, macOS, iOS, Android
🔄 Real-time validacija
Takojšnje povratne informacije uporabniku
🎨 Custom komponente
Ponovno uporabne StatusMessage komponente
🔍 Iskanje & Sortiranje
Hitro filtriranje lokalnih podatkov
💾 Optimizirana UI
ObservableCollection za avtomatske posodobitve
⚡ Async/Await
Ne-blokajoča asinhronska komunikacija
🌐 Naloga 8 - Blazor WebAssembly Aplikacija
Pregled aplikacije
Blazor WebAssembly aplikacija je spletna single-page aplikacija (SPA) za upravljanje produktov.
Uporablja API https://dummyjson.com/products in vključuje napredne funkcionalnosti:
- ✅ CRUD operacije nad produkti
- ✅ Iskanje in filtriranje po imenu
- ✅ Sortiranje po ceni (naraščajoče/padajoče)
- ✅ 4 različni Chart.js grafi za vizualizacijo
- ✅ Responsive dizajn (Bootstrap 5)
- ✅ JavaScript interop
- ✅ Custom dialog komponenta
- ✅ Validacija podatkov
Arhitektura projekta
Blazor/
├── Program.cs # Konfiguracija aplikacije
├── Pages/
│ ├── Products.razor # Glavna stran s produkti
│ ├── EditProduct.razor # Dodajanje/urejanje produkta
│ ├── Home.razor # Domača stran
│ ├── Counter.razor # Demo stran
│ └── Weather.razor # Demo stran
├── Models/
│ └── Product.cs # Model produkta z validacijo
├── Services/
│ └── ProductService.cs # Servis za API operacije
├── Shared/
│ ├── ConfirmDialog.razor # Custom dialog komponenta
│ ├── MainLayout.razor # Osnovna postavitev
│ └── NavMenu.razor # Navigacijski meni
└── wwwroot/
├── index.html # Root HTML + Chart.js setup
└── css/app.css # Custom CSS styling
1. Model podatkov - Product.cs
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "Naslov je obvezen")]
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
[Range(0, 10000, ErrorMessage = "Cena mora biti pozitivna")]
public decimal Price { get; set; }
public double Rating { get; set; }
public int Stock { get; set; }
public string Category { get; set; } = "General";
public string Thumbnail { get; set; }
}
Data Annotations:
[Required]- polje je obvezno[Range(0, 10000)]- cena mora biti med 0 in 10000- Omogoča avtomatsko validacijo v
<EditForm>komponentah
2. ProductService - Simulacija API klicev
💡 Lokalna simulacija
Pomembno: Ker DummyJSON API ne omogoča resničnih sprememb podatkov,
servis uporablja lokalno simulacijo s statično listo _lokalniProdukti.
Prvi klic naloži podatke iz API-ja, nadaljnje operacije se izvajajo lokalno.
📥 GET - Pridobi vse produkte
public async Task<List<Product>> GetProductsAsync()
{
if (!_soPodatkiNalozeni)
{
var response = await _http.GetFromJsonAsync<ProductResponse>("products?limit=10");
_lokalniProdukti = response.Products;
_soPodatkiNalozeni = true;
}
return _lokalniProdukti;
}
Prvi klic: naloži 10 produktov iz API-ja in jih shrani lokalno.
Nadaljnji klici: vrne lokalno shranjene podatke (brez dodatnih HTTP zahtev).
🔍 GET BY ID
public async Task<Product> GetProductByIdAsync(int id)
{
var product = _lokalniProdukti.FirstOrDefault(p => p.Id == id);
return await Task.FromResult(product);
}
➕ ADD - Dodajanje
public async Task AddProductAsync(Product product)
{
product.Id = _lokalniProdukti.Max(p => p.Id) + 1;
product.Thumbnail = "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg";
_lokalniProdukti.Add(product);
await Task.CompletedTask;
}
Generira nov ID (Max(Id) + 1), nastavi dummy sliko in doda v lokalno listo.
✏️ UPDATE - Posodabljanje
public async Task UpdateProductAsync(int id, Product product)
{
var obstojeci = _lokalniProdukti.FirstOrDefault(p => p.Id == id);
if (obstojeci != null)
{
obstojeci.Title = product.Title;
obstojeci.Price = product.Price;
obstojeci.Stock = product.Stock;
}
await Task.CompletedTask;
}
🗑️ DELETE - Brisanje
public async Task DeleteProductAsync(int id)
{
var item = _lokalniProdukti.FirstOrDefault(p => p.Id == id);
if (item != null)
{
_lokalniProdukti.Remove(item);
}
await Task.CompletedTask;
}
3. Products.razor - Glavna stran
🎨 UI struktura
@page "/products"
@using Blazor.Models
@using Blazor.Services
@inject ProductService ProductService
@inject IJSRuntime JS
<h3>Upravljanje Produktov</h3>
<!-- Iskanje + Sortiranje + Dodaj -->
<div class="row mb-3">
<div class="col-md-4">
<input @bind="searchTerm" @bind:event="oninput" />
</div>
<div class="col-md-4">
<button @onclick="SortByPrice">
Sortiraj po ceni: @(isAscending ? "⬆" : "⬇")
</button>
</div>
<div class="col-md-4">
<a href="/product/add" class="btn btn-success">Dodaj</a>
</div>
</div>
📊 4 Chart.js grafi
<div class="card mb-4">
<div class="card-header">Vizualizacija podatkov</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<canvas id="chartPrice"></canvas>
</div>
<div class="col-md-3">
<canvas id="chartStock"></canvas>
</div>
<div class="col-md-3">
<canvas id="chartRating"></canvas>
</div>
<div class="col-md-3">
<canvas id="chartValue"></canvas>
</div>
</div>
</div>
</div>
📋 Tabela s produkti
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Slika</th>
<th>Naziv</th>
<th>Kategorija</th>
<th>Cena</th>
<th>Zaloga</th>
<th>Akcije</th>
</tr>
</thead>
<tbody>
@foreach (var p in FilteredProducts)
{
<tr>
<td>
<img src="@p.Thumbnail" alt="@p.Title"
style="width: 50px; height: 50px;"/>
</td>
<td><strong>@p.Title</strong></td>
<td>@p.Category</td>
<td>@p.Price €</td>
<td>
@if(p.Stock < 10)
{
<span class="text-danger">@p.Stock (Nizka!)</span>
}
else
{
<span>@p.Stock</span>
}
</td>
<td>
<a href="/product/edit/@p.Id">Uredi</a>
<button @onclick="() => RequestDelete(p)">Izbriši</button>
</td>
</tr>
}
</tbody>
</table>
Pogojno oblikovanje: Če je zaloga (Stock) manjša od 10, se prikaže v rdeči barvi z opozorilom "Nizka!".
4. C# Logika - Products.razor @code
🔍 Filtriranje in sortiranje
private List<Product> products;
private string searchTerm = "";
private bool isAscending = true;
private IEnumerable<Product> FilteredProducts
{
get
{
if (products == null) return new List<Product>();
var res = products.AsEnumerable();
// 1. Filtriranje (Iskanje)
if (!string.IsNullOrWhiteSpace(searchTerm))
{
res = res.Where(p =>
p.Title.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
}
// 2. Sortiranje
if (isAscending)
res = res.OrderBy(p => p.Price);
else
res = res.OrderByDescending(p => p.Price);
return res;
}
}
Computed property: FilteredProducts je dinamična lastnost,
ki se avtomatsko preračuna ob spremembi searchTerm ali isAscending.
UI se avtomatsko posodobi brez dodatnih klicev.
📊 Chart.js integracija
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && products != null)
{
await GenerateCharts();
}
}
private async Task GenerateCharts()
{
var labels = products.Take(10)
.Select(p => p.Title.Length > 10
? p.Title.Substring(0, 10) + "..."
: p.Title)
.ToArray();
var top10Products = products.Take(10).ToList();
// 1. Graf Cen (Line Chart)
await JS.InvokeVoidAsync("setupChart", "chartPrice", new
{
type = "line",
data = new
{
labels = labels,
datasets = new[] {
new {
label = "Cena (€)",
data = top10Products.Select(p => p.Price),
borderColor = "blue",
tension = 0.4
}
}
}
});
// 2. Graf Zaloge (Bar Chart)
await JS.InvokeVoidAsync("setupChart", "chartStock", new
{
type = "bar",
data = new
{
labels = labels,
datasets = new[] {
new {
label = "Zaloga (kos)",
data = top10Products.Select(p => p.Stock),
backgroundColor = "green"
}
}
}
});
// 3. Graf Ocen (Bar Chart)
await JS.InvokeVoidAsync("setupChart", "chartRating", new
{
type = "bar",
data = new
{
labels = labels,
datasets = new[] {
new {
label = "Ocena (1-5)",
data = top10Products.Select(p => p.Rating),
backgroundColor = "orange"
}
}
}
});
// 4. Graf Vrednosti zaloge (Line Chart with fill)
await JS.InvokeVoidAsync("setupChart", "chartValue", new
{
type = "line",
data = new
{
labels = labels,
datasets = new[] {
new {
label = "Vrednost zaloge (€)",
data = top10Products.Select(p => p.Price * p.Stock),
borderColor = "purple",
fill = true,
backgroundColor = "rgba(128, 0, 128, 0.2)"
}
}
}
});
}
JavaScript Interop:
OnAfterRenderAsync(firstRender)- kliče se po renderiranju komponentefirstRender- zagotavlja, da se grafi narišejo samo enkratJS.InvokeVoidAsync("setupChart", ...)- kliče JavaScript funkcijo iz C#- Podatki se posredujejo kot anonymous object, ki se avtomatsko serializira v JSON
🗑️ Brisanje z dialogom
private bool showDeleteDialog = false;
private Product productToDelete;
private void RequestDelete(Product p)
{
productToDelete = p;
showDeleteDialog = true;
}
private async Task HandleDeleteConfirmation(bool confirmed)
{
showDeleteDialog = false;
if (confirmed && productToDelete != null)
{
await ProductService.DeleteProductAsync(productToDelete.Id);
products = await ProductService.GetProductsAsync();
await JS.InvokeVoidAsync("showAlert",
$"Produkt '{productToDelete.Title}' je bil uspešno izbrisan!");
productToDelete = null;
}
}
Two-way dialog komunikacija:
1. RequestDelete() prikaže dialog
2. Dialog sproži OnConfirmationChange event
3. HandleDeleteConfirmation() obdela rezultat
4. Če je potrjeno, izbriše produkt in prikaže JS alert
5. EditProduct.razor - Dodajanje/Urejanje
🔀 Dinamična ruta
@page "/product/add"
@page "/product/edit/{Id:int}"
[Parameter] public int Id { get; set; }
Ista komponenta obdeluje dve različni poti:
/product/add- Id = 0 (dodajanje)/product/edit/5- Id = 5 (urejanje)
📝 EditForm z validacijo
<EditForm Model="product" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label>Naziv</label>
<InputText class="form-control" @bind-Value="product.Title" />
</div>
<div class="mb-3">
<label>Cena</label>
<InputNumber class="form-control" @bind-Value="product.Price" />
</div>
<div class="mb-3">
<label>Zaloga</label>
<InputNumber class="form-control" @bind-Value="product.Stock" />
</div>
<button type="submit">Shrani</button>
<button type="button" @onclick="Cancel">Prekliči</button>
</EditForm>
Blazor validacija:
<DataAnnotationsValidator />- aktivira validacijo iz [Required], [Range] atributov<ValidationSummary />- prikaže vse napake na enem mestuOnValidSubmit- sproži se SAMO če je forma veljavna<InputText>, <InputNumber>- Blazor komponente z avtomatsko validacijo
💾 Shranjevanje logike
protected override async Task OnInitializedAsync()
{
if (Id != 0)
{
product = await ProductService.GetProductByIdAsync(Id);
}
}
private async Task HandleSubmit()
{
if (Id == 0)
{
await ProductService.AddProductAsync(product);
}
else
{
await ProductService.UpdateProductAsync(Id, product);
}
NavManager.NavigateTo("/products");
}
private void Cancel()
{
NavManager.NavigateTo("/products");
}
OnInitializedAsync() - če Id != 0, naloži obstoječ produkt za urejanje.
HandleSubmit() - glede na Id odloči, ali gre za dodajanje ali posodobitev.
NavigationManager - programatska navigacija na drugo stran.
6. ConfirmDialog.razor - Custom komponenta
🎨 Modal dialog UI
@if (Show)
{
<div class="modal fade show d-block"
style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5>Potrditev</h5>
<button @onclick="Close">×</button>
</div>
<div class="modal-body">
<p>@Message</p>
</div>
<div class="modal-footer">
<button @onclick="Close">Prekliči</button>
<button @onclick="Confirm">Potrdi</button>
</div>
</div>
</div>
</div>
}
🔄 EventCallback komunikacija
@code {
[Parameter] public bool Show { get; set; }
[Parameter] public string Message { get; set; } = "Ali ste prepričani?";
[Parameter] public EventCallback<bool> OnConfirmationChange { get; set; }
private async Task Close()
=> await OnConfirmationChange.InvokeAsync(false);
private async Task Confirm()
=> await OnConfirmationChange.InvokeAsync(true);
}
Parametri:
Show- kontrolira vidnost dialogaMessage- besedilo za prikazOnConfirmationChange- callback, ki pošlje rezultat (true/false) nazaj na starševsko komponento
📲 Uporaba komponente
<ConfirmDialog
Show="showDeleteDialog"
Message="@($"Ali res želite izbrisati produkt '{productToDelete?.Title}'?")"
OnConfirmationChange="HandleDeleteConfirmation" />
7. JavaScript Interop - index.html
📦 Chart.js setup
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
window.setupChart = function(canvasId, config) {
var ctx = document.getElementById(canvasId).getContext('2d');
// Destroy existing chart if exists
if (window[canvasId + '_chart']) {
window[canvasId + '_chart'].destroy();
}
window[canvasId + '_chart'] = new Chart(ctx, config);
}
window.showAlert = function(message) {
alert(message);
}
</script>
Funkcionalnosti:
setupChart()- inicializira Chart.js graf z konfiguracijo- Shranjuje graf v
window[canvasId + '_chart']za kasnejše uničenje showAlert()- prikaže preprosto JavaScript opozorilo- Te funkcije se kličejo iz C# preko
JSRuntime
8. Responsive Design
📱 Bootstrap Grid sistem
<div class="row">
<div class="col-md-4">Iskanje</div>
<div class="col-md-4">Sortiranje</div>
<div class="col-md-4">Dodaj gumb</div>
</div>
<!-- Grafi -->
<div class="row">
<div class="col-md-3">Graf 1</div>
<div class="col-md-3">Graf 2</div>
<div class="col-md-3">Graf 3</div>
<div class="col-md-3">Graf 4</div>
</div>
Breakpoint system:
col-md-4- na srednje velikih zaslonih (≥768px) zavzame 4/12 (33.33%)col-md-3- na srednje velikih zaslonih zavzame 3/12 (25%)- Na manjših zaslonih se stolpci zložijo vertikalno (100% širine)
📊 Responsive tabela
<div class="table-responsive">
<table class="table table-striped table-hover">
<!-- Tabela -->
</table>
</div>
table-responsive omogoča horizontalno drsenje tabele na majhnih zaslonih.
✅ Izpolnjene zahteve - Blazor
| Zahteva | Implementacija | Lokacija |
|---|---|---|
| Pridobivanje podatkov iz API-ja | ✅ ProductService.GetProductsAsync() | Services/ProductService.cs |
| CRUD operacije | ✅ Add, Update, Delete metode | Services/ProductService.cs |
| Iskanje in filtriranje | ✅ FilteredProducts computed property | Products.razor |
| Preverjanje veljavnosti | ✅ DataAnnotations + EditForm | Product.cs, EditProduct.razor |
| Prikaz sporočil o napakah | ✅ ValidationSummary, JS alerts | EditProduct.razor, Products.razor |
| Navigacija med stranmi | ✅ NavigationManager, NavMenu | EditProduct.razor, Shared/NavMenu.razor |
| Odzivna oblika | ✅ Bootstrap 5 grid sistem | Products.razor, MainLayout.razor |
| Vizualizacija podatkov | ✅ Chart.js integracija | Products.razor, index.html |
| Vsaj 4 grafi | ✅ Cena (line), Zaloga (bar), Ocena (bar), Vrednost (line fill) | Products.razor |
| Brez vnašanja ID-jev | ✅ Parametri v URL, direktni objekti | Products.razor, EditProduct.razor |
| JavaScript funkcija | ✅ setupChart(), showAlert() | wwwroot/index.html |
| Vsaj 1 komponenta | ✅ ConfirmDialog | Shared/ConfirmDialog.razor |
🎯 Ključne funkcionalnosti
🌐 WebAssembly
Deluje v brskalniku brez strežnika
📊 4 različni grafi
Line charts, bar charts z Chart.js
🎨 Bootstrap 5
Responsive design za vse naprave
✅ Validacija
Data Annotations + EditForm
🔧 JS Interop
Integracija Chart.js iz C#
🔄 Reactive UI
Avtomatska posodobitev ob spremembah
🤖 Naloga 9 - Gemini LLM Integracija
Pregled funkcionalnosti
V Blazor aplikacijo je integriran Google Gemini LLM za dinamično generiranje produktnih predlogov. Ključne značilnosti:
- ✅ Dinamičen prompt - sestavlja se glede na uporabniški vnos (kategorija, proračun, ciljna skupina)
- ✅ Strukturiran JSON odgovor - LLM vrača podatke v določenem formatu
- ✅ Deserializacija v C# objekte - avtomatsko pretvorba JSON → C# modeli
- ✅ Real-time validacija - preverjanje vnosov pred klicem API-ja
- ✅ Error handling - obravnavanje napak, mock podatki za testiranje
Arhitektura rešitve
Blazor/
├── Models/
│ └── GeminiModels.cs # Vsi modeli za Gemini API
│ ├── GeminiRequest # Request structure
│ ├── GeminiResponse # Response structure
│ ├── ProductSuggestion # Domenski model
│ └── ProductSuggestionsResponse
├── Services/
│ └── GeminiService.cs # Logika za Gemini API klice
└── Pages/
└── AISuggestions.razor # UI za AI asistenta
1. Modeli - GeminiModels.cs
🔹 API Request/Response modeli
public class GeminiRequest
{
[JsonPropertyName("contents")]
public List<GeminiRequestContent> Contents { get; set; }
[JsonPropertyName("generationConfig")]
public GeminiGenerationConfig GenerationConfig { get; set; }
}
public class GeminiResponse
{
[JsonPropertyName("candidates")]
public List<GeminiCandidate> Candidates { get; set; }
}
Modeli za komunikacijo z Gemini API. Uporablja [JsonPropertyName] atribute
za ujemanje s camelCase poimenovanjem v JSON-u.
🔹 Domenski modeli (strukturiran LLM odgovor)
public class ProductSuggestion
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("estimatedPrice")]
public decimal EstimatedPrice { get; set; }
[JsonPropertyName("category")]
public string Category { get; set; }
[JsonPropertyName("targetAudience")]
public string TargetAudience { get; set; }
}
public class ProductSuggestionsResponse
{
[JsonPropertyName("suggestions")]
public List<ProductSuggestion> Suggestions { get; set; }
[JsonPropertyName("summary")]
public string Summary { get; set; }
}
Pomembno: Ti modeli definirajo strukturo, ki jo LLM MORA vrniti. To omogoča programsko obdelavo odgovora namesto parseanja prostega besedila.
2. Gemini Servis - GeminiService.cs
🔧 Konstrukcija dinamičnega prompta
public async Task<ProductSuggestionsResponse> GetProductSuggestionsAsync(
string category, string budget, string targetAudience)
{
// DINAMIČEN PROMPT - sestavljen glede na uporabniški vnos
string dynamicPrompt = $@"
You are a product recommendation assistant. Based on the following user preferences,
suggest 3 relevant products.
User Preferences:
- Category: {category}
- Budget: {budget} EUR
- Target Audience: {targetAudience}
IMPORTANT: Respond ONLY with valid JSON in this exact format:
{{
""suggestions"": [
{{
""name"": ""Product name"",
""description"": ""Brief description"",
""estimatedPrice"": 50.00,
""category"": ""{category}"",
""targetAudience"": ""{targetAudience}""
}}
],
""summary"": ""Brief summary of suggestions""
}}
Generate 3 product suggestions that fit the budget and target audience.";
// ... pošlji na API
}
Ključni pristop:
- Prompt se dinamično sestavlja z
$@"..."string interpolacijo - Vključuje TOČNO strukturo JSON-a, ki jo pričakujemo
- Navodilo "Respond ONLY with valid JSON" je kritično za zanesljivo parseanje
📡 Klic Gemini API
// Pripravi request
var geminiRequest = new GeminiRequest
{
Contents = new List<GeminiRequestContent>
{
new GeminiRequestContent
{
Parts = new List<GeminiRequestPart>
{
new GeminiRequestPart { Text = dynamicPrompt }
}
}
},
GenerationConfig = new GeminiGenerationConfig
{
Temperature = 0.7, // Kreativnost modela (0.0 - 1.0)
MaxOutputTokens = 1024
}
};
// Pošlji na API - POMEMBNO: API ključ mora biti v headerju!
var request = new HttpRequestMessage(HttpMethod.Post, BASE_URL);
request.Headers.Add("X-goog-api-key", API_KEY);
request.Content = JsonContent.Create(geminiRequest);
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"Gemini API error: {response.StatusCode} - {errorContent}");
}
// Pridobi odgovor
var geminiResponse = await response.Content.ReadFromJsonAsync<GeminiResponse>();
var rawText = geminiResponse?.Candidates?.FirstOrDefault()
?.Content?.Parts?.FirstOrDefault()?.Text;
🔄 Parsiranje JSON odgovora
// Odstrani markdown code blocks (če jih LLM doda)
var jsonText = rawText.Trim();
if (jsonText.StartsWith("```json"))
{
jsonText = jsonText.Substring(7);
}
if (jsonText.EndsWith("```"))
{
jsonText = jsonText.Substring(0, jsonText.Length - 3);
}
jsonText = jsonText.Trim();
// DESERIALIZACIJA v C# objekte
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var productSuggestions = JsonSerializer.Deserialize<ProductSuggestionsResponse>(
jsonText,
options
);
return productSuggestions;
LLM včasih vrne JSON zavit v markdown code blocks (```json ... ```).
Ta koda to odstrani in deserializira JSON v ProductSuggestionsResponse objekt.
3. UI - AISuggestions.razor
📝 Uporabniški vnos
<div class="row">
<div class="col-md-4">
<label>Kategorija produkta:</label>
<select @bind="selectedCategory">
<option value="Elektronika">Elektronika</option>
<option value="Pohištvo">Pohištvo</option>
<!-- ... -->
</select>
</div>
<div class="col-md-4">
<label>Proračun (EUR):</label>
<input type="number" @bind="budget" />
</div>
<div class="col-md-4">
<label>Ciljna skupina:</label>
<select @bind="targetAudience">
<option value="Otroci">Otroci</option>
<option value="Odrasli">Odrasli</option>
<!-- ... -->
</select>
</div>
</div>
<button @onclick="GetSuggestions" disabled="@isLoading">
@if (isLoading)
{
<span class="spinner-border"></span> Generiranje...
}
else
{
🚀 Pridobi AI predloge
}
</button>
🎨 Prikaz rezultatov
@if (suggestions != null)
{
<div class="card">
<div class="card-header">✅ AI generiran povzetek</div>
<div class="card-body">
<p>@suggestions.Summary</p>
</div>
</div>
<div class="row">
@foreach (var suggestion in suggestions.Suggestions)
{
<div class="col-md-4">
<div class="card">
<h5>@suggestion.Name</h5>
<p>@suggestion.Description</p>
<h4 class="text-success">@suggestion.EstimatedPrice €</h4>
<span class="badge">@suggestion.Category</span>
<span class="badge">@suggestion.TargetAudience</span>
</div>
</div>
}
</div>
<!-- Raw JSON za debugging -->
<pre><code>@rawJsonResponse</code></pre>
}
💻 C# Logika
@code {
private string selectedCategory = "";
private int budget = 100;
private string targetAudience = "";
private bool isLoading = false;
private ProductSuggestionsResponse suggestions;
private async Task GetSuggestions()
{
// Validacija
if (string.IsNullOrEmpty(selectedCategory))
{
errorMessage = "Prosim izberi kategorijo!";
return;
}
isLoading = true;
try
{
// KLIC GEMINI API
suggestions = await GeminiService.GetProductSuggestionsAsync(
selectedCategory,
budget.ToString(),
targetAudience
);
// Shrani JSON za debugging
rawJsonResponse = JsonSerializer.Serialize(suggestions,
new JsonSerializerOptions { WriteIndented = true });
}
catch (Exception ex)
{
errorMessage = $"Napaka: {ex.Message}";
}
finally
{
isLoading = false;
}
}
}
4. Nastavitev API ključa
📋 Koraki za pridobitev ključa:
- Pojdi na Google AI Studio
- Klikni na "Get API Key"
- Ustvari nov projekt (če še nimate)
- Kopiraj API ključ
- V
GeminiService.cszamenjajYOUR_GEMINI_API_KEY_HEREs pravim ključem
🔒 Produkcijska varnost
// ❌ NAROBE - hard-coded ključ v kodi
private const string API_KEY = "Kljucek...";
// ✅ PRAVILNO - iz environment variable ali appsettings.json
private readonly string _apiKey;
public GeminiService(IConfiguration configuration)
{
_apiKey = configuration["Gemini:ApiKey"];
}
// appsettings.json:
{
"Gemini": {
"ApiKey": "YOUR_API_KEY_HERE"
}
}
V produkciji NIKOLI ne shrani API ključev direktno v kodo! Uporabi konfiguracijsko datoteko
ali environment variable, ki niso v verzijskem sistemu (dodaj v .gitignore).
✅ Izpolnjene zahteve - Gemini LLM
| Zahteva | Implementacija | Lokacija |
|---|---|---|
| Dinamičen prompt (ne statičen) | ✅ Prompt sestavljen iz category, budget, targetAudience | GeminiService.cs |
| Klic na strežniku | ✅ HTTP klic v GeminiService (lahko premakneš v WebAPI backend) | Services/GeminiService.cs |
| Prikaz na odjemalcu | ✅ Blazor stran z Razor komponentami | Pages/AISuggestions.razor |
| Strukturiran JSON odgovor | ✅ LLM vrne JSON z določeno strukturo | ProductSuggestionsResponse model |
| Deserializacija v C# razred | ✅ JsonSerializer.Deserialize<ProductSuggestionsResponse> | GeminiService.cs |
🎯 Testiranje brez API ključa
Servis vključuje mock podatke za testiranje:
private ProductSuggestionsResponse GetMockSuggestions(
string category, string targetAudience)
{
return new ProductSuggestionsResponse
{
Summary = $"Mock predlogi za '{category}'",
Suggestions = new List<ProductSuggestion>
{
new ProductSuggestion
{
Name = $"{category} - Premium",
Description = "Visokokakovostna opcija",
EstimatedPrice = 99.99m,
Category = category,
TargetAudience = targetAudience
},
// ... več predlogov
}
};
}
Če API klic ne uspe (napačen ključ, omrežna napaka), se avtomatsko uporabijo mock podatki. To omogoča testiranje UI brez delujoče AI integracije.
🚀 Primer delovanja
1️⃣ Uporabnik vnese
Kategorija: "Elektronika"
Proračun: 500 EUR
Skupina: "Profesionalci"
2️⃣ Sestavljen prompt
"Suggest 3 products in category Elektronika, budget 500 EUR, for Profesionalci..."
3️⃣ Gemini vrne JSON
{ "suggestions": [ { "name": "Pametna tipkovnica", ...} ] }
4️⃣ Prikaz na UI
3 kartice s produktnimi predlogi, povzetek, cene
🎓 Zaključek
Primerjava tehnologij
| Aspect | .NET MAUI | Blazor WebAssembly |
|---|---|---|
| Platforma | Windows, macOS, iOS, Android | Spletni brskalnik |
| UI Sintaksa | XAML | Razor (HTML-like) |
| Dostop do naprave | Popoln (kamere, GPS, senzorji) | Omejen (le brskalnik API) |
| Distribucija | App stores, instalacijske datoteke | Enostavna (URL link) |
| Performance | Native hitrost | WebAssembly (blizu native) |
| Offline support | Vgrajeno | Potreben PWA setup |
Naučene lekcije
- Async/Await: Kritično za ne-blokajoče UI operacije pri API klicih
- ObservableCollection (MAUI): Avtomatska posodobitev UI brez ročnega refresha
- Computed properties (Blazor): Reaktivne lastnosti za dinamično filtriranje
- Component reusability: Custom komponente (StatusMessage, ConfirmDialog) zmanjšajo podvajanje kode
- Validation patterns: Data Annotations + EditForm zagotavljajo robustno validacijo
- JavaScript Interop: Omogoča uporabo obstoječih JS knjižnic (Chart.js) v Blazor
- REST API best practices: Centraliziran servis za vse API klice, pravilno obravnavanje status kodov
Možne nadgradnje
🔐 Avtentikacija
JWT tokens, OAuth2
💾 Lokalna baza
SQLite za offline-first pristop
🔔 Push notifikacije
Real-time obvestila (MAUI)
📸 Delo s slikami
Upload, crop, filters
🌍 Lokalizacija
Večjezična podpora
🎨 Teme
Dark/Light mode toggle
📋 Povzetek
Projekt zajema implementacijo treh naprednih .NET funkcionalnosti:
- MAUI Aplikacija - Cross-platform native aplikacija za upravljanje objav z intuitivnim UI
- Blazor Aplikacija - Spletna aplikacija s podatkovnimi vizualizacijami in CRUD operacijami
- Gemini LLM Integracija - AI asistent za dinamično generiranje produktnih predlogov
Vse zahteve iz navodil (CRUD operacije, validacija, sortiranje, iskanje, komponente, grafi, responsive design, LLM integracija) so bile uspešno implementirane z best practice pristopom.
3
Naloge
100%
Zahtev izpolnjenih
10+
Komponent
1
AI Model