Zpracování souběžnosti s rozhraním Entity Framework v aplikaci ASP.NET MVC (7 z 10)
Ukázková webová aplikace Contoso University ukazuje, jak vytvářet aplikace ASP.NET MVC 4 pomocí entity Framework 5 Code First a sady Visual Studio 2012. Informace o sérii kurzů najdete v prvním kurzu v této sérii.
Poznámka
Pokud narazíte na problém, který nemůžete vyřešit, stáhněte si dokončenou kapitolu a zkuste problém reprodukovat. Obecně můžete najít řešení problému porovnáním kódu s dokončeným kódem. Informace o některých běžných chybách a jejich řešení najdete v tématu Chyby a alternativní řešení.
V předchozích dvou kurzech jste pracovali se souvisejícími daty. Tento kurz ukazuje, jak zpracovávat souběžnost. Vytvoříte webové stránky, které s entitou Department
pracují, a stránky, které upravují a odstraňují Department
entity, budou zpracovávat chyby souběžnosti. Následující ilustrace znázorňují stránky Index a Delete, včetně některých zpráv, které se zobrazí v případě konfliktu souběžnosti.
Konflikty souběžnosti
Ke konfliktu souběžnosti dojde, když jeden uživatel zobrazí data entity, aby ji mohl upravit, a pak jiný uživatel aktualizuje data stejné entity před zápisem změny prvního uživatele do databáze. Pokud zjišťování takových konfliktů nepovolíte, ten, kdo naposledy aktualizuje databázi, přepíše změny druhého uživatele. V mnoha aplikacích je toto riziko přijatelné: pokud existuje jen málo uživatelů nebo málo aktualizací nebo pokud nejsou skutečně kritické, pokud jsou některé změny přepsány, náklady na programování pro souběžnost mohou převážit výhody. V takovém případě nemusíte aplikaci konfigurovat tak, aby zpracovávala konflikty souběžnosti.
Pesimistická souběžnost (zamykání)
Pokud vaše aplikace potřebuje zabránit náhodné ztrátě dat ve scénářích souběžnosti, jedním ze způsobů, jak to udělat, je použít zámky databáze. Tomu se říká pesimistická souběžnost. Například před čtením řádku z databáze si vyžádáte zámek jen pro čtení nebo pro přístup k aktualizacím. Pokud uzamknete řádek pro přístup k aktualizacím, žádní jiní uživatelé nebudou moct zamknout řádek jen pro čtení nebo pro přístup k aktualizacím, protože by získali kopii dat, která se právě mění. Pokud řádek uzamknete pro přístup jen pro čtení, ostatní ho můžou také uzamknout pro přístup jen pro čtení, ale ne pro aktualizaci.
Správa zámků má nevýhody. Může být složité programovat. Vyžaduje významné prostředky pro správu databází a může způsobovat problémy s výkonem, když se zvýší počet uživatelů aplikace (to znamená, že se dobře škáluje). Z těchto důvodů pesimistická souběžnost nepodporují všechny systémy pro správu databází. Entity Framework neposkytuje žádnou integrovanou podporu a tento kurz neukazuje, jak ho implementovat.
Optimistická metoda souběžného zpracování
Alternativou k pesimistické souběžnosti je optimistická souběžnost. Optimistická souběžnost znamená, že povolíte, aby ke konfliktům souběžnosti docházelo, a pokud ano, pak odpovídajícím způsobem reagovat. Jan například spustí stránku Úpravy oddělení a změní částku rozpočtu pro anglické oddělení z 350 000,00 USD na 0,00 USD.
Než Jan klikne na Uložit, Jana spustí stejnou stránku a změní pole Počáteční datum z 1. 9. 2007 na 8. 8. 2013.
Jan nejprve klikne na Uložit a uvidí svou změnu, když se prohlížeč vrátí na stránku Index, a pak jan klikne na Uložit. To, co se stane dál, závisí na způsobu zpracování konfliktů souběžnosti. Mezi tyto možnosti patří:
Můžete sledovat, kterou vlastnost uživatel změnil, a aktualizovat pouze odpovídající sloupce v databázi. V ukázkovém scénáři by nedošlo ke ztrátě dat, protože dva uživatelé aktualizovali různé vlastnosti. Až si někdo příště projde anglické oddělení, uvidí změny Johna i Jane – počáteční datum 8. 8. 2013 a rozpočet nula dolarů.
Tato metoda aktualizace může snížit počet konfliktů, které by mohly vést ke ztrátě dat, ale nemůže se vyhnout ztrátě dat, pokud jsou provedeny konkurenční změny stejné vlastnosti entity. To, jestli Entity Framework funguje tímto způsobem, závisí na způsobu implementace aktualizačního kódu. Ve webové aplikaci to často není praktické, protože může vyžadovat udržování velkého množství stavu, aby bylo možné sledovat všechny původní hodnoty vlastností pro entitu i nové hodnoty. Udržování velkého množství stavu může ovlivnit výkon aplikace, protože vyžaduje prostředky serveru nebo musí být zahrnuta do samotné webové stránky (například do skrytých polí).
Můžete nechat Janovu změnu přepsat. Až si někdo příště projde anglické oddělení, uvidí 8. 8. 2013 a obnovenou hodnotu 350 000,00 USD. Tento scénář se nazývá Klient wins nebo Last ve scénáři Wins . (Hodnoty klienta mají přednost před tím, co je v úložišti dat.) Jak je uvedeno v úvodu k této části, pokud nezakódujete pro zpracování souběžnosti, dojde k tomu automaticky.
V databázi můžete zabránit, aby se změny Jane aktualizovaly. Obvykle byste zobrazili chybovou zprávu, zobrazili byste jí aktuální stav dat a povolili jí, aby změny znovu použila, pokud je stále chce provést. Tento scénář se nazývá Store Wins . (Hodnoty úložiště dat mají přednost před hodnotami odeslanými klientem.) V tomto kurzu implementujete scénář služby Store Wins. Tato metoda zajišťuje, že se nepřepíšou žádné změny, aniž by uživatel byl upozorněn na to, co se děje.
Zjišťování konfliktů souběžnosti
Konflikty můžete vyřešit zpracováním výjimek OptimisticConcurrencyException , které entity Framework vyvolává. Aby bylo možné zjistit, kdy tyto výjimky vyvolat, musí být Rozhraní Entity Framework schopné detekovat konflikty. Proto musíte odpovídajícím způsobem nakonfigurovat databázi a datový model. Mezi možnosti povolení detekce konfliktů patří:
V tabulce databáze zahrňte sledovací sloupec, pomocí kterého můžete určit, kdy byl řádek změněn. Pak můžete nakonfigurovat Entity Framework tak, aby zahrnoval tento sloupec do klauzule
Where
SQLUpdate
neboDelete
příkazů.Datový typ sledovacího sloupce je obvykle rowversion. Hodnota rowversion je pořadové číslo, které se při každé aktualizaci řádku zvýší.
Update
V příkazu neboDelete
klauzuleWhere
obsahuje původní hodnotu sledovacího sloupce (původní verzi řádku). Pokud aktualizovaný řádek změnil jiný uživatel, hodnota verowversion
sloupci se liší od původní hodnoty, takžeUpdate
příkaz orDelete
nemůže kvůli klauzuli najít řádek, který se má aktualizovatWhere
. Když Entity Framework zjistí, že příkazem neboDelete
nebyly aktualizoványUpdate
žádné řádky (to znamená, že počet ovlivněných řádků je nulový), interpretuje to jako konflikt souběžnosti.Nakonfigurujte Entity Framework tak, aby zahrnoval původní hodnoty všech sloupců v tabulce v klauzuli
Where
Update
aDelete
příkazů.Stejně jako u první možnosti platí, že pokud se od prvního přečtení řádku něco na řádku změnilo,
Where
klauzule nevrátí řádek, který se má aktualizovat, což Entity Framework interpretuje jako konflikt souběžnosti. U databázových tabulek, které mají mnoho sloupců, může mít tento přístup za následek velmi velkéWhere
klauzule a může vyžadovat udržování velkých objemů stavu. Jak jsme si poznamenali dříve, udržování velkých objemů stavu může ovlivnit výkon aplikace, protože buď vyžaduje prostředky serveru, nebo musí být součástí samotné webové stránky. Proto se tento přístup obecně nedoporučuje a není to metoda použitá v tomto kurzu.Pokud chcete implementovat tento přístup ke souběžnosti, musíte v entitě, pro kterou chcete souběžnost sledovat, označit všechny vlastnosti, které nejsou primárním klíčem, přidáním atributu ConcurrencyCheck k nim. Tato změna umožňuje Entity Frameworku zahrnout všechny sloupce v klauzuli
UPDATE
SQLWHERE
příkazů.
Ve zbývající části tohoto kurzu přidáte do Department
entity vlastnost sledování rowversion, vytvoříte kontroler a zobrazení a otestujete, abyste ověřili, že vše funguje správně.
Přidání vlastnosti optimistické souběžnosti do entity oddělení
V souboru Models\Department.cs přidejte vlastnost sledování s názvem RowVersion
:
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
public DateTime StartDate { get; set; }
[Display(Name = "Administrator")]
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
Atribut časového razítka určuje, že tento sloupec bude zahrnut do Where
klauzule Update
a Delete
příkazů odeslaných do databáze. Atribut se nazývá Časové razítko, protože předchozí verze SQL Server používaly datový typ časového razítka SQL předtím, než ho nahradila verze řádků SQL. Typ .Net pro rowversion
je bajtové pole. Pokud dáváte přednost použití rozhraní FLUENT API, můžete pomocí metody IsConcurrencyToken určit vlastnost tracking, jak je znázorněno v následujícím příkladu:
modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
Projděte si problém s githubem Replace IsConcurrencyToken by IsRowVersion.
Přidáním vlastnosti jste změnili model databáze, takže musíte provést další migraci. V konzole Správce balíčků (PMC) zadejte následující příkazy:
Add-Migration RowVersion
Update-Database
Vytvoření kontroleru oddělení
Pomocí Department
následujícího nastavení vytvořte kontroler a zobrazení stejným způsobem jako u ostatních kontrolerů:
V souboru Controllers\DepartmentController.cs přidejte using
příkaz:
using System.Data.Entity.Infrastructure;
Změňte "Příjmení" na "FullName" všude v tomto souboru (čtyři výskyty), aby rozevírací seznamy správce oddělení obsahovaly celé jméno instruktora, a ne jenom příjmení.
ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName");
Nahraďte existující kód pro metodu HttpPost
Edit
následujícím kódem:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
Department department)
{
try
{
if (ModelState.IsValid)
{
db.Entry(department).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();
if (databaseValues.Name != clientValues.Name)
ModelState.AddModelError("Name", "Current value: "
+ databaseValues.Name);
if (databaseValues.Budget != clientValues.Budget)
ModelState.AddModelError("Budget", "Current value: "
+ String.Format("{0:c}", databaseValues.Budget));
if (databaseValues.StartDate != clientValues.StartDate)
ModelState.AddModelError("StartDate", "Current value: "
+ String.Format("{0:d}", databaseValues.StartDate));
if (databaseValues.InstructorID != clientValues.InstructorID)
ModelState.AddModelError("InstructorID", "Current value: "
+ db.Instructors.Find(databaseValues.InstructorID).FullName);
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
department.RowVersion = databaseValues.RowVersion;
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName", department.InstructorID);
return View(department);
}
Zobrazení uloží původní RowVersion
hodnotu do skrytého pole. Když pořadač modelu vytvoří department
instanci, bude mít tento objekt původní RowVersion
hodnotu vlastnosti a nové hodnoty pro ostatní vlastnosti, jak uživatel zadal na stránce Upravit. Když pak Entity Framework vytvoří příkaz SQL UPDATE
, bude tento příkaz obsahovat WHERE
klauzuli, která hledá řádek, který má původní RowVersion
hodnotu.
Pokud příkaz neovlivní UPDATE
žádné řádky (žádné řádky nemají původní RowVersion
hodnotu), Entity Framework vyvolá DbUpdateConcurrencyException
výjimku a kód v catch
bloku získá ovlivněnou Department
entitu z objektu výjimky. Tato entita má hodnoty načtené z databáze i nové hodnoty zadané uživatelem:
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().ToObject();
Dále kód přidá vlastní chybovou zprávu pro každý sloupec, který obsahuje hodnoty databáze, které se liší od toho, co uživatel zadal na stránce Upravit:
if (databaseValues.Name != currentValues.Name)
ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
// ...
Delší chybová zpráva vysvětluje, co se stalo a co s tím dělat:
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The"
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
Nakonec kód nastaví RowVersion
hodnotu objektu Department
na novou hodnotu načtenou z databáze. Tato nová RowVersion
hodnota bude uložena ve skrytém poli při opětovném zobrazení stránky Pro úpravy a při dalším kliknutí uživatele na Uložit budou zachyceny pouze chyby souběžnosti, ke kterým dochází, protože se znovu zobrazí stránka Upravit.
Do pole Views\Department\Edit.cshtml přidejte skryté pole pro uložení RowVersion
hodnoty vlastnosti hned za skryté pole vlastnosti DepartmentID
:
@model ContosoUniversity.Models.Department
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<fieldset>
<legend>Department</legend>
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
V souboru Views\Department\Index.cshtml nahraďte stávající kód následujícím kódem, kterým přesunete odkazy řádků doleva a změníte název stránky a záhlaví sloupců tak, aby se zobrazovaly FullName
místo LastName
ve sloupci Administrator :
@model IEnumerable<ContosoUniversity.Models.Department>
@{
ViewBag.Title = "Departments";
}
<h2>Departments</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th></th>
<th>Name</th>
<th>Budget</th>
<th>Start Date</th>
<th>Administrator</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
@Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
</td>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Administrator.FullName)
</td>
</tr>
}
</table>
Testování zpracování optimistické souběžnosti
Spusťte web a klikněte na Oddělení:
Klikněte pravým tlačítkem na hypertextový odkaz Upravit pro Kim Abercrombie, vyberte Otevřít na nové kartě a pak klikněte na upravit hypertextový odkaz pro Kim Abercrombie. Obě okna zobrazují stejné informace.
Změňte pole v prvním okně prohlížeče a klikněte na Uložit.
Prohlížeč zobrazí stránku Index se změněnou hodnotou.
Změňte libovolné pole v druhém okně prohlížeče a klikněte na Uložit.
V druhém okně prohlížeče klikněte na Uložit . Zobrazí se chybová zpráva:
Znovu klikněte na Uložit . Hodnota, kterou jste zadali v druhém prohlížeči, se uloží spolu s původní hodnotou dat, která jste změnili v prvním prohlížeči. Když se zobrazí stránka Index, zobrazí se uložené hodnoty.
Aktualizace stránky Pro odstranění
Na stránce Odstranit Zjistí Entity Framework konflikty souběžnosti způsobené tím, že někdo jiný upravuje oddělení podobným způsobem. HttpGet
Delete
Když metoda zobrazí potvrzovací zobrazení, bude zobrazení obsahovat původní RowVersion
hodnotu ve skrytém poli. Tato hodnota je pak k dispozici pro metodu HttpPost
Delete
, která se volá, když uživatel potvrdí odstranění. Když Entity Framework vytvoří příkaz SQL DELETE
, obsahuje WHERE
klauzuli s původní RowVersion
hodnotou. Pokud má příkaz za následek nulový počet řádků (to znamená, že řádek byl změněn po zobrazení potvrzovací stránky Odstranit), vyvolá se výjimka souběžnosti a HttpGet Delete
zavolá se metoda s příznakem chyby nastaveným na true
hodnotu , aby se znovu zobrazila potvrzovací stránka s chybovou zprávou. Je také možné, že nulový počet řádků byl ovlivněn, protože řádek byl odstraněn jiným uživatelem, takže v takovém případě se zobrazí jiná chybová zpráva.
V souboru DepartmentController.cs nahraďte metodu HttpGet
Delete
následujícím kódem:
public ActionResult Delete(int id, bool? concurrencyError)
{
Department department = db.Departments.Find(id);
if (concurrencyError.GetValueOrDefault())
{
if (department == null)
{
ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was deleted by another user after you got the original values. "
+ "Click the Back to List hyperlink.";
}
else
{
ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you got the original values. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
}
}
return View(department);
}
Metoda přijímá volitelný parametr, který označuje, jestli je stránka po chybě souběžnosti znovu zobrazena. Pokud je true
tento příznak , odešle se do zobrazení chybová zpráva pomocí ViewBag
vlastnosti .
Nahraďte kód v HttpPost
Delete
metodě (s názvem DeleteConfirmed
) následujícím kódem:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(Department department)
{
try
{
db.Entry(department).State = EntityState.Deleted;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToAction("Delete", new { concurrencyError=true } );
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
return View(department);
}
}
V kódu, který jste právě nahradili, tato metoda přijala pouze ID záznamu:
public ActionResult DeleteConfirmed(int id)
Tento parametr jste změnili na Department
instanci entity vytvořenou pořadačem modelu. Získáte tak přístup k hodnotě RowVersion
vlastnosti kromě klíče záznamu.
public ActionResult Delete(Department department)
Také jste změnili název metody akce z DeleteConfirmed
na Delete
. Vygenerovaný kód s názvem HttpPost
Delete
metoda DeleteConfirmed
dává HttpPost
metodě jedinečný podpis. (ClR vyžaduje, aby přetížené metody měly různé parametry metody.) Teď, když jsou podpisy jedinečné, můžete zůstat u konvence MVC a použít stejný název pro HttpPost
metody a HttpGet
delete.
Pokud dojde k chybě souběžnosti, kód znovu zobrazí potvrzovací stránku Odstranit a poskytne příznak, který označuje, že by se měla zobrazit chybová zpráva souběžnosti.
V souboru Views\Department\Delete.cshtml nahraďte vygenerovaný kód následujícím kódem, který provede některé změny formátování a přidá pole s chybovou zprávou. Změny jsou zvýrazněné.
@model ContosoUniversity.Models.Department
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<p class="error">@ViewBag.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<fieldset>
<legend>Department</legend>
<div class="display-label">
@Html.DisplayNameFor(model => model.Name)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.Budget)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Budget)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.StartDate)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.StartDate)
</div>
<div class="display-label">
@Html.DisplayNameFor(model => model.Administrator.FullName)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Administrator.FullName)
</div>
</fieldset>
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
<p>
<input type="submit" value="Delete" /> |
@Html.ActionLink("Back to List", "Index")
</p>
}
Tento kód přidá mezi nadpisy a h3
chybovou h2
zprávu:
<p class="error">@ViewBag.ConcurrencyErrorMessage</p>
V poli se nahradí LastName
za FullName
Administrator
:
<div class="display-label">
@Html.LabelFor(model => model.InstructorID)
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Administrator.FullName)
</div>
Nakonec přidá za příkaz skrytá pole pro DepartmentID
vlastnosti Html.BeginForm
a RowVersion
:
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
Spusťte stránku Index oddělení. Klikněte pravým tlačítkem myši na hypertextový odkaz Odstranit pro anglické oddělení a vyberte Otevřít v novém okně a pak v prvním okně klikněte na hypertextový odkaz Upravit pro anglické oddělení.
V prvním okně změňte jednu z hodnot a klikněte na Uložit :
Stránka Index změnu potvrdí.
V druhém okně klikněte na Odstranit.
Zobrazí se chybová zpráva o souběžnosti a hodnoty Oddělení se aktualizují s tím, co je aktuálně v databázi.
Pokud znovu kliknete na Odstranit , budete přesměrováni na stránku Rejstřík, která ukazuje, že oddělení bylo odstraněno.
Souhrn
Tím se dokončí úvod do zpracování konfliktů souběžnosti. Informace o dalších způsobech zpracování různých scénářů souběžnosti najdete v tématech Vzorce optimistické souběžnosti a Práce s hodnotami vlastností na blogu týmu Entity Framework. V dalším kurzu se dozvíte, jak implementovat dědičnost tabulek v hierarchii pro Instructor
entity a Student
.
Odkazy na další prostředky Entity Framework najdete v mapě obsahu ASP.NET Data Access.
Váš názor
https://aka.ms/ContentUserFeedback.
Připravujeme: V průběhu roku 2024 budeme postupně vyřazovat problémy z GitHub coby mechanismus zpětné vazby pro obsah a nahrazovat ho novým systémem zpětné vazby. Další informace naleznete v tématu:Odeslat a zobrazit názory pro