Generic method to set the filename
At creating the video for AccountPeriodFilenamesViewModel it turned the the filenames can be given in the TextBox but there was no Select button.
private async Task SelectFileAsync<TRow>(object obj) { if (!await StatementFolderGetter.DoesStatementFolderExistWithWarningAsync()) return; var statementFolder = await StatementFolderGetter.GetAsync(); var (rowGetter, boxedDisplayObject, propertySetter) = (( Func<MainDbContext, int, ValueTask<TRow>>, object, Action<TRow, string>))obj; var displayObject = boxedDisplayObject as IHasId; using var dbContext = MainDbContextFactory.Create(); var rowInDb = await rowGetter(dbContext, displayObject.Id); var openFileDialog = new OpenFileDialog { InitialDirectory = statementFolder }; if (openFileDialog.ShowDialog() == false) return; var path = openFileDialog.FileName; if (!path.StartsWith(statementFolder)) { MainWindowViewModel.LoggerViewModel.AddLineWithTime( Resource.TheSelectedFileIsNotInTheStatementFolder + statementFolder, true); return; } path = path.CutFirstNCharacter(statementFolder.Length + 1); propertySetter(rowInDb, path); await dbContext.SaveChangesAsync(); await PagerViewModel.OnDbTableChangedOneByOneAsync(true); }
So a method is needed which selects the file and sets the value in the Db. Moreover, this method should be generic so it can be used for both history and statement file. The last point made the problem difficult.
We have to check if the statement folder is valid, if it is then we need its value.
We have to decode the parameter. SelectFileAsync is used to create the Execute method of an ICommand so it can have a single object parameter. If you have more than one parameter then you have to put these parameters into an object. For example, we have 3 parameters, we create a triplet from them, and now we deconstruct these parameters. The first parameter is a function, rowGetter, which returns the Db row of the given table corresponding to the selected row. The second parameter is the selected row, boxedDisplayObject. The third parameter is a method, propertySetter, which determines which column is to be set.
From selected row we need only the Id property, so we cast this object to IHasId interface. Then we create the dbContext and use the rowGetter. Usually we do not pass the dbContext as a parameter but this is an exception. If MainDbContextFactory.Create is used inside the rowGetter method and we return the Db row then the tracking of the row is ended at the return command. So we got the correct row, we can modify it but SaveChangesAsync does not save the changes in the Db and there will be no error message at all!
Next, OpenFileDialog is used to select the file. If the file is not in the statement folder then the path is not saved. Else the beginning of the path is cut because only the relative path is stored.
Finally, the relative path is set, the changes are saved into the Db and an event is fired to indicate that the Db is changed. It will implicitly refresh the actual table.
// These variables cannot be in-line. var rowGetter = (MainDbContext dbContext, int id) => dbContext.AccountPeriodFilenames.FindAsync(id); var historyFilenameSetter = (AccountPeriodFilename accountPeriodFilename, string newValue) => { accountPeriodFilename.HistoryFilename = newValue; }; var statementFilenameSetter = (AccountPeriodFilename accountPeriodFilename, string newValue) => { accountPeriodFilename.StatementFilename = newValue; }; SelectHistoryFileCommand = new RelayCommandGuiSwitcherAsync( obj => SelectFileAsync<AccountPeriodFilename>((rowGetter, obj, historyFilenameSetter)), MainWindowViewModelStore); SelectBalanceFileCommand = new RelayCommandGuiSwitcherAsync( obj => SelectFileAsync<AccountPeriodFilename>((obj, statementFilenameSetter)), MainWindowViewModelStore);
Two commands are needed to use the SelectFileAsync method. First, the parameters must be defined. rowGetter returns a row from dbContext.AccountPeriodFilenames. historyFilenameSetter sets the HistoryFilename property of passed parameter. RelayCommandGuiSwitcherAsync inherits from RelayCommandAsync. obj is the selected row which comes from the xaml file. SelectFileAsync has two opening parenthesis. The first is needed because this is a method, the second because a triplet is created.
Using DI
This was not easy but the fun part only comes now. There are other tabs where this functionality is useful so it makes sense to create a class an put it into the DI (dependency injection) container.
public class FilenameSetterInDb<TDbView, TDbTable, TDisplayClass>( StatementFolderGetter statementFolderGetter, IMainDbContextFactory mainDbContextFactory, Func<TableViewModelsStore, PagerViewModel<TDbView, TDbTable, TDisplayClass>> pagerViewModelGetter, MainWindowViewModelStore mainWindowViewModelStore) where TDbView : class, IHasId, new() where TDbTable : class, IHasId, new() where TDisplayClass : class, IHasId, IDataErrorInfo, new() { public async Task SelectAndSetAsync(object obj) { if (!await statementFolderGetter.DoesStatementFolderExistWithWarningAsync()) return; var statementFolder = await statementFolderGetter.GetAsync(); var (rowGetter, boxedDisplayObject, propertySetter) = (( Func<MainDbContext, int, ValueTask<TDbTable>>, object, Action<TDbTable, string>))obj; var hasIdObject = boxedDisplayObject as IHasId; using var dbContext = mainDbContextFactory.Create(); var rowInDb = await rowGetter(dbContext, hasIdObject.Id); var openFileDialog = new OpenFileDialog { InitialDirectory = statementFolder }; if (openFileDialog.ShowDialog() == false) return; var path = openFileDialog.FileName; var mainWindowViewModel = mainWindowViewModelStore.MainWindowViewModel; if (!path.StartsWith(statementFolder)) { mainWindowViewModel.LoggerViewModel.AddLineWithTime( Resource.TheSelectedFileIsNotInTheStatementFolder + statementFolder, true); return; } path = path.CutFirstNCharacter(statementFolder.Length + 1); propertySetter(rowInDb, path); await dbContext.SaveChangesAsync(); await pagerViewModelGetter(mainWindowViewModel.TableViewModelsStore) .OnDbTableChangedOneByOneAsync(true); } }
Primary constructor is used to get statementFolderGetter, mainDbContextFactory, pagerViewModelGetter, mainWindowViewModelStore. From mainWindowViewModelStore the we can get mainWindowViewModel. A store is usually used if we want to avoid a loop in DI resolution. pagerViewModelGetter is a function which returns the pagerViewModel. It is used in the last row to make the Db change notification.
services.AddSingleton<Func<TableViewModelsStore, PagerViewModel< ViewAccountBalanceFilename, AccountBalanceFilename, DisplayAccountBalanceFilename>>>(s => tableViewModelsStore => tableViewModelsStore.AccountBalanceFilenamesViewModel.PagerViewModel); services.AddSingleton<FilenameSetterInDb< ViewAccountBalanceFilename, AccountBalanceFilename, DisplayAccountBalanceFilename>>();
// These variables cannot be in-line. var rowGetter = (MainDbContext dbContext, int id) => dbContext.AccountPeriodFilenames.FindAsync(id); var historyFilenameSetter = (AccountPeriodFilename accountPeriodFilename, string newValue) => { accountPeriodFilename.HistoryFilename = newValue; }; SelectHistoryFileCommand = new RelayCommandGuiSwitcherAsync( obj => FilenameSetterInDb.SelectAndSetAsync((rowGetter, obj, historyFilenameSetter)), MainWindowViewModelStore);