Adding Ratings and Saving YouTube Study Cards to SharePoint Online
Part 2 — Adding Ratings and Saving YouTube Study Cards to SharePoint Online (WPF + MVVM + Microsoft Graph)
Goal
Extend the WPF YouTube Cards app so you can:
- rate a video (0–10 or stars)
- add notes and a study status
- save/update that record in SharePoint Online
- later filter your personal study library (“Favorites”, “To Watch”, etc.)
This turns your YouTube search UI into a personal learning database.
Why Microsoft Graph for SharePoint Lists
Microsoft Graph provides a modern, consistent API for SharePoint lists:
- Create list items
- Update existing items
- Query/filter items
- Works with OAuth (Entra ID) and standard enterprise auth patterns
This is the cleanest long-term route for a “study app” that stores structured data in SharePoint.
Data Model
UI Model (Card) additions
Add the following fields to your VideoCard:
Rating(int 0–10)Status(string: ToWatch / Watching / Watched / Favorite)Notes(string)SavedToSharePoint(bool)- optional:
LastSavedAt
These fields live in the card so you can edit and save per video.
SharePoint List Schema (Example)
Create a SharePoint list (placeholder names):
Site: https://contoso.sharepoint.com/sites/LearningHub
List title: Learning Videos
Columns:
Title(Single line text) → store video titleVideoId(Single line text) → unique keyVideoUrl(Hyperlink)Channel(Single line text)ThumbnailUrl(Single line text)SearchQuery(Single line text)Rating(Number)Status(Choice: ToWatch, Watching, Watched, Favorite)Notes(Multiple lines text)
✅ Important: keep VideoId indexed (performance) and treat it as your “natural key”.
Authentication (Public-Safe Guidance)
For a desktop WPF app, the recommended flow is:
- Register an app in Entra ID (Azure AD)
- Use MSAL (
Microsoft.Identity.Client) - Acquire a token for Microsoft Graph
You’ll need Graph permissions for SharePoint list operations. In a real environment, you choose the minimal permission model your admin allows.
Public note: never embed real IDs/secrets. Use placeholders and environment config.
Graph Endpoints You’ll Use
You have two main strategies to target a list:
Strategy A — Use SiteId and ListId (fast and stable)
- Resolve
siteIdonce - Resolve
listIdonce - Then call list item endpoints
Strategy B — Use Site path addressing (readable)
Graph supports site path patterns, but for list operations, you often end up using IDs anyway.
App Architecture Update
Add a “SharePoint layer”:
SharePointVideoRepositoryFindByVideoIdAsync(videoId)CreateAsync(videoCard)UpdateAsync(itemId, videoCard)UpsertAsync(videoCard)
Your ViewModel calls this repository when a user clicks “Save”.
This keeps MVVM clean.
UI Changes (WPF)
In each card, add:
- rating dropdown (0–10) OR a slider
- status dropdown
- notes TextBox (small)
- Save button
Example UI concept:
- Card footer: Open | Save
- Under title: rating/status controls
- Description stays read-only
Implementation: Core Building Blocks (Code)
1) Update the VideoCard model
public int Rating { get; set; } // 0..10public string Status { get; set; } // ToWatch / Watching / Watched / Favoritepublic string Notes { get; set; }public bool SavedToSharePoint { get; set; }
2) Create an Auth provider with MSAL (Graph token)
// PSEUDO / TEMPLATE (placeholders)var app = PublicClientApplicationBuilder .Create("YOUR_CLIENT_ID") .WithAuthority("https://login.microsoftonline.com/YOUR_TENANT_ID") .WithRedirectUri("http://localhost") .Build();string[] scopes = new[] { "https://graph.microsoft.com/.default" };// or delegated scopes like: "Sites.ReadWrite.All" depending on your modelvar result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();var accessToken = result.AccessToken;
You then attach that token to HTTP requests to Graph.
3) Create a minimal Graph client wrapper
public sealed class GraphClient{ private readonly Func<Task<string>> _getToken; public GraphClient(Func<Task<string>> getToken) { _getToken = getToken; } public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage req) { var token = await _getToken(); req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); using (var http = new HttpClient()) { return await http.SendAsync(req); } }}
4) Repository: Upsert logic
Pseudo-steps:
- Query list items filtered by
VideoId - If found → PATCH
- Else → POST create
Find by VideoId
GET /sites/{siteId}/lists/{listId}/items?expand=fields&$filter=fields/VideoId eq '{videoId}'
Create item
POST /sites/{siteId}/lists/{listId}/items{ "fields": { "Title": "...", "VideoId": "...", "VideoUrl": "...", "Channel": "...", "ThumbnailUrl": "...", "SearchQuery": "...", "Rating": 8, "Status": "Watched", "Notes": "..." }}
Update item
PATCH /sites/{siteId}/lists/{listId}/items/{itemId}/fields{ "Rating": 9, "Status": "Favorite", "Notes": "Updated notes..."}
MVVM Wiring: “Save” Command Per Card
You can do this cleanly by making VideoCard carry commands, or you keep the command in the parent VM and pass the card as parameter.
Example:
SaveVideoCommandinMainViewModelCommandParameter="{Binding}"
Then inside VM:
private async Task SaveVideoAsync(VideoCard card){ await _repo.UpsertAsync(card); card.SavedToSharePoint = true;}
Common Pitfalls and Fixes
1) Cross-thread exceptions (WPF)
Any update to ObservableCollection or UI-bound properties must happen on UI thread.
✅ Fix: keep await without ConfigureAwait(false) inside UI code, or marshal to Dispatcher.
2) Duplicates in SharePoint
If you don’t filter by VideoId, you’ll create duplicates.
✅ Fix: always upsert by VideoId.
3) SharePoint column internal names
SharePoint field internal name may differ from display name.
✅ Fix:
- create columns carefully
- verify internal names in list settings or via API once
- use internal names in Graph payload.
