C
C#ā€¢16mo ago
Mekasu0124

ā” Creating A Share Button in avalonia

I have a button
<Button Grid.Column="2" Command="{Binding $parent[Window].DataContext.ShareEntry}" Content="Share Entry" />
<Button Grid.Column="2" Command="{Binding $parent[Window].DataContext.ShareEntry}" Content="Share Entry" />
This button is bound to a function in the MainWindowViewModel.cs file
public void ShareEntry()
{

}
public void ShareEntry()
{

}
When this button is clicked, I'm wanting it to take the selected item the user has chosen in the DataGrid and pretty print it to a microsoft word document file, open the users file explorer so that the user can select where they want to save the file to, and then create and save the file to that desired location. I have zero idea how to do this. Thanks in advance
73 Replies
Mekasu0124
Mekasu0124OPā€¢16mo ago
public void ShareEntry()
{
Document doc = new Document();
DocumentBuilder builder = new DocumentBuilder();

Font font = builder.Font;
font.Size = 32;
font.Bold = false;
font.Underline = Underline.None;
font.Color = System.Drawing.Color.Black;
font.Name = "Times New Roman";

builder.Writeln($"Date: {List.SelectedItem.Date}");
builder.Writeln($"Time: {List.SelectedItem.Time}");
builder.Writeln($"Title: {List.SelectedItem.Title}");
builder.Writeln($"Details: {List.SelectedItem.Description}");
}
public void ShareEntry()
{
Document doc = new Document();
DocumentBuilder builder = new DocumentBuilder();

Font font = builder.Font;
font.Size = 32;
font.Bold = false;
font.Underline = Underline.None;
font.Color = System.Drawing.Color.Black;
font.Name = "Times New Roman";

builder.Writeln($"Date: {List.SelectedItem.Date}");
builder.Writeln($"Time: {List.SelectedItem.Time}");
builder.Writeln($"Title: {List.SelectedItem.Title}");
builder.Writeln($"Details: {List.SelectedItem.Description}");
}
I found the Aspose.Words library for creating a word doc file, but I haven't written in doc.Save(filename); because I need to be able to open the file explorer and get access to where the user chose to save the file https://stackoverflow.com/questions/1132422/open-a-folder-using-process-start https://stackoverflow.com/questions/12822337/c-sharp-get-explorer-exe-to-return-a-file-path I've also tried using these two stack overflows as reference for workign with the file dialog thing, but I'm not making any progress...
Kouhai
Kouhaiā€¢16mo ago
Mekasu0124
Mekasu0124OPā€¢16mo ago
do I have to build an entire ViewModels file just for this or can I do this in my current function and just make the function asyncronous?
Kouhai
Kouhaiā€¢16mo ago
If you just want to make it work, yes you can do it in your current function
Mekasu0124
Mekasu0124OPā€¢16mo ago
it's just a simple asking the user where they want to save the file and then creating the word doc and saving it to that location. but if I need to make an entire usercontrol for it, then I can
Kouhai
Kouhaiā€¢16mo ago
You don't need to make a user control for it, in MVVM you might abstract it behind a service, but that abstraction is not really needed in this case
Mekasu0124
Mekasu0124OPā€¢16mo ago
I don't know what "abstract it behind a service" means, but I didn't think it'd be a whole bunch of code to do it lol
Kouhai
Kouhaiā€¢16mo ago
Btw you should bind SelectedItem instead of getting it from the control itself
Mekasu0124
Mekasu0124OPā€¢16mo ago
I dislike preference programming lol one dev will tell me to bind it to a variable and use that variable and another dev will tell me I don't need to do that and just call it directly....so confusing
Kouhai
Kouhaiā€¢16mo ago
Basically, you might structure your code something like
> Services (folder)
> IFileService.cs (Interface)
> FileService.cs (Default implementation for that interface)
> Services (folder)
> IFileService.cs (Interface)
> FileService.cs (Default implementation for that interface)
You then would simply have an instance of IFileService in your view model, and open the save dialog and get IStorageFile from it, the idea behind that is the view model doesn't really care about how it got the IStorageFile, so it's abstracted away from it (You don't have the implementation in the ViewModel itself)
Mekasu0124
Mekasu0124OPā€¢16mo ago
if this is more efficient, then I do not mind learning it this way
Kouhai
Kouhaiā€¢16mo ago
Binding is actually the right MVVM way, because your view model should not really have access to controls directly Sure, so you might declare an interface
internal IFileService
{
public Task<IStorageFile> GetSaveFile();
}
internal IFileService
{
public Task<IStorageFile> GetSaveFile();
}
And then implement it like avalonia docs suggest (difference would returning an IStorageFile instead of directly opening it and writing to it)
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok so I created the two files that you suggested would be MVVM appropriate. IStorageFile is erroring because I don't know the import
Mekasu0124
Mekasu0124OPā€¢16mo ago
unless it's not an import
Mekasu0124
Mekasu0124OPā€¢16mo ago
that code and the picture are two different things to me
Mekasu0124
Mekasu0124OPā€¢16mo ago
is the picture the FileService.cs part? like I'd use the FileService.cs file to create the text file with writing the information to it and such, and then use the other file to get the save location and save it?
Kouhai
Kouhaiā€¢16mo ago
oops sorry I didn't clarify that IFileService is an interface not a class, btw place the cursor over IStorageFile visual studio should suggest the namespace to add The idea is that the view model doesn't really care about how it got the IStorageFile instance, it simply needs gets one from an IFileService instance and then writes to it Note the capital I in both IStorageFile and IFileService They are interfaces
Mekasu0124
Mekasu0124OPā€¢16mo ago
so I have to build an interface for it? like an axaml file?
Kouhai
Kouhaiā€¢16mo ago
interfaces are basically contracts, you can't instantiate an interface by itself, but a class/struct can implement them You declare an interface like this
interface IMyService
{
// ...
}
interface IMyService
{
// ...
}
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok so I have the interface
// Services/IFileService.cs
using System.Threading.Tasks;

namespace Diary.Services
{
internal class IFileService
{
}
}
// Services/IFileService.cs
using System.Threading.Tasks;

namespace Diary.Services
{
internal class IFileService
{
}
}
what am I supposed to do with it? I do apologize for being dumb. Learning this for the first time and wanna go step by step
Kouhai
Kouhaiā€¢16mo ago
It's totally fine! You now need to declare what methods/properties you expect the interface to have In this case you'd need public Task<IStorageFile> GetSaveFile(); (notice how the method doesn't have a body, it's simply a "contract"
Mekasu0124
Mekasu0124OPā€¢16mo ago
oh ok so no { } needed for it
Mekasu0124
Mekasu0124OPā€¢16mo ago
so when putting that line in, it's not wanting a namespace, it's wanting to create another interface. Is that correct?
Mekasu0124
Mekasu0124OPā€¢16mo ago
or do I have line 5 wrong? instead of internal class it should be interface
Kouhai
Kouhaiā€¢16mo ago
Hmm, no, which version of avalonia are you using?
Mekasu0124
Mekasu0124OPā€¢16mo ago
0.10.21 if I update the version, I have to go back through the entire project and change almost everything. I learned this version because the Todo App tutorial on avalonia is written off 0.10.21 and they haven't updated to 11 yet I don't see anything on the docs site when changing it from 11 to 0.10.x for user interfaces or dialogs. Was it not a thing?
Kouhai
Kouhaiā€¢16mo ago
Well, 0.10.x had a different dialog api, which would make it harder to implement MVVM, because SaveFileDialog.ShowAsync takes in a window instance, so you'd have do something like this I think you had a dependency on ReactiveUI?
Mekasu0124
Mekasu0124OPā€¢16mo ago
I am using ReactiveUI over not CommunityToolkit how difficult is it going to make this project efficient? If I need to re-write it in version 11, I can work on that and then get back to you once I've done that but it'll take me a couple of days and if I update to version 11, I'll be switching from ReactiveUI to CommunityToolkit so that I can use [ReactiveCommand] for my commands which tbh I should probably do
Kouhai
Kouhaiā€¢16mo ago
You don't need to switch to version 11, basically you want a way to invoke a method on your MainWindow instance, so something like
protected override void OnInitialized()
{
if(DataContext is MainWindowViewModel vm)
{
vm.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog);
}
}
private async Task ShowSaveFileDialog(InteractionContext<Unit, string?> interaction)
{
var dialog = new SaveFileDialog();
var filename = await dialog.ShowAsync(this);
interaction.SetOutput(filename);
}
protected override void OnInitialized()
{
if(DataContext is MainWindowViewModel vm)
{
vm.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog);
}
}
private async Task ShowSaveFileDialog(InteractionContext<Unit, string?> interaction)
{
var dialog = new SaveFileDialog();
var filename = await dialog.ShowAsync(this);
interaction.SetOutput(filename);
}
(this is in MainWindow.axaml.cs)
Mekasu0124
Mekasu0124OPā€¢16mo ago
so I no longer need the services files I created? or do I still need them
using Avalonia.Controls;

namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
using Avalonia.Controls;

namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
and this is my current MainWindow.axaml.cs file. If I incorporate that, would it be like
using Avalonia.Controls;
namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public override void OnInitialized()
{
if (DataContext is MainWindowViewModel vm)
...
using Avalonia.Controls;
namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public override void OnInitialized()
{
if (DataContext is MainWindowViewModel vm)
...
? or would it take place of the public MainWindow()? like
namespace Diary.Views
{
public partial class MainWindow : Window
{
public override void MainWindow()
{ ... }
namespace Diary.Views
{
public partial class MainWindow : Window
{
public override void MainWindow()
{ ... }
Kouhai
Kouhaiā€¢16mo ago
No no, like what you did here
using Avalonia.Controls;
namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public override void OnInitialized()
{
if (DataContext is MainWindowViewModel vm)
...
using Avalonia.Controls;
namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public override void OnInitialized()
{
if (DataContext is MainWindowViewModel vm)
...
You can keep them if you want to refactor the code later
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok I'm going to remove them for now so I don't get confused
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok so here's what I've got
Mekasu0124
Mekasu0124OPā€¢16mo ago
and that error is wanting me to do one of these
Kouhai
Kouhaiā€¢16mo ago
What this code does is register a handler for a ReactiveUI interaction, ReactiveUI interaction is a basically a callback that allows you to provide a handler that takes input and provides output
Interaction<Unit, string?>
^ ^
Output
Input
Interaction<Unit, string?>
^ ^
Output
Input
Because you don't want to provide any input to the SaveFileDialoge, you can pass Unit In the viemodels constructror initialize it
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok I accidentally clicked Introduce local for 'vm.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog) and although the error went away from ShowSaveFileDialog, I have no idea what it did and where it did it at. It didn't do anything to this file found it so in the MainWindowViewModel, I want to create
namespace Diary.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
internal readonly object ShowSaveFileDialog;
ViewModelBase _content;
namespace Diary.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
internal readonly object ShowSaveFileDialog;
ViewModelBase _content;
right?
Kouhai
Kouhaiā€¢16mo ago
Remove internal readonly object ShowSaveFileDialog; And in the constructor do ShowSaveFileDialog = new Interaction<Unit, string?>(); Which would initialize the property ShowSaveFileDialog to a new ReactiveUI Interaction
Mekasu0124
Mekasu0124OPā€¢16mo ago
and that's in the constructor of the MainWindowViewModel correct?
Kouhai
Kouhaiā€¢16mo ago
Yup!
Mekasu0124
Mekasu0124OPā€¢16mo ago
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok so I'm missing some things
Mekasu0124
Mekasu0124OPā€¢16mo ago
or does it belong there my bad
Mekasu0124
Mekasu0124OPā€¢16mo ago
I know it goes in one of those two places......
Kouhai
Kouhaiā€¢16mo ago
Here, that would be the constructor!
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok cool. so now it's telling me that ShowSaveFileDialog doesn't exist in this instance so do I need to pass it as a paremeter? or should I generate a new field above the constructor
Kouhai
Kouhaiā€¢16mo ago
You need to readonly property for it A readonly proeprty looks something like this PropertyType PropertyName { get; }
Mekasu0124
Mekasu0124OPā€¢16mo ago
namespace Diary.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
ViewModelBase _content;

public MainWindowViewModel(Database db)
{
ShowSaveFileDialog = new Interaction<Unit, string?>();
Content = List = new DiaryListViewModel(db);
}

public readonly Interaction<Unit, string?> ShowSaveFileDialog { get; }
namespace Diary.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
ViewModelBase _content;

public MainWindowViewModel(Database db)
{
ShowSaveFileDialog = new Interaction<Unit, string?>();
Content = List = new DiaryListViewModel(db);
}

public readonly Interaction<Unit, string?> ShowSaveFileDialog { get; }
ok so now we have this but you said read only so public readonly ...
Kouhai
Kouhaiā€¢16mo ago
You can't add readonly to properties To make a property readonly you provide a getter and no setter
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok I removed it
Kouhai
Kouhaiā€¢16mo ago
Okay so now to simply get a file from the showsafefiledialog Interaction provides a method Handle which will call the handler you registered earlier So ShowSaveFileDialog.Handle(Unit.Default) will call private async Task ShowSaveFileDialog(InteractionContext<Unit, string?> interaction) (The one in the MainWindow) Make sure to await Handle You can call ShowSaveFileDialog.Handle(Unit.Default) wherever you defined the saving procedure.
Mekasu0124
Mekasu0124OPā€¢16mo ago
public void ShareEntry()
{
var saveDialog = ShowSaveFileDialog.Handle(Unit.Default);
}
public void ShareEntry()
{
var saveDialog = ShowSaveFileDialog.Handle(Unit.Default);
}
the share button in DiaryListVIew.axaml is bound to the ShareEntry() function in MainWindowVIewModel which is where we just did
namespace Diary.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
ViewModelBase _content;

public MainWindowViewModel(Database db)
{
ShowSaveFileDialog = new Interaction<Unit, string?>();
Content = List = new DiaryListViewModel(db);
}

public readonly Interaction<Unit, string?> ShowSaveFileDialog { get; }
namespace Diary.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
ViewModelBase _content;

public MainWindowViewModel(Database db)
{
ShowSaveFileDialog = new Interaction<Unit, string?>();
Content = List = new DiaryListViewModel(db);
}

public readonly Interaction<Unit, string?> ShowSaveFileDialog { get; }
Mekasu0124
Mekasu0124OPā€¢16mo ago
they're now in the same file
Mekasu0124
Mekasu0124OPā€¢16mo ago
the button is bound like this
<Button Grid.Column="2" Command="{Binding $parent[Window].DataContext.ShareEntry}" Content="Share Entry" />
<Button Grid.Column="2" Command="{Binding $parent[Window].DataContext.ShareEntry}" Content="Share Entry" />
Kouhai
Kouhaiā€¢16mo ago
Make sure to await Handle so ShareEntry should be public async Task ShareEntry()
Mekasu0124
Mekasu0124OPā€¢16mo ago
so this will be an async function then
Mekasu0124
Mekasu0124OPā€¢16mo ago
Kouhai
Kouhaiā€¢16mo ago
Yup, any function that awaits should be async
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok cool so what's next? because the thought of flow is - Prompt user via file explorer on where to save the exported information - save file location to a variable - create the microsoft word document - save the new document to the desired file location
Kouhai
Kouhaiā€¢16mo ago
Well, SaveEntry now will show the user a save file dialog And the file location is the value returned from ShowSaveFileDialog.Handle
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok so I'm not sure if I was supposed to test the program to see if the file explorer window opened or not, but I did and when I clicked the share entry button, I got this
Kouhai
Kouhaiā€¢16mo ago
You didn't register a handler Paste the code for MainWindow Not MainWindowViewModel MainWindow.axaml.cs
Mekasu0124
Mekasu0124OPā€¢16mo ago
using Avalonia.Controls;
using Diary.ViewModels;
using ReactiveUI;
using System.Reactive;
using System.Threading.Tasks;

namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
protected override void OnInitialized()
{
if (DataContext is MainWindowViewModel vm)
{
vm.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog);
}
}
private async Task ShowSaveFileDialog(InteractionContext<Unit, string?> interaction)
{
var dialog = new SaveFileDialog();
var filename = await dialog.ShowAsync(this);
interaction.SetOutput(filename);
}
}
}
using Avalonia.Controls;
using Diary.ViewModels;
using ReactiveUI;
using System.Reactive;
using System.Threading.Tasks;

namespace Diary.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
protected override void OnInitialized()
{
if (DataContext is MainWindowViewModel vm)
{
vm.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog);
}
}
private async Task ShowSaveFileDialog(InteractionContext<Unit, string?> interaction)
{
var dialog = new SaveFileDialog();
var filename = await dialog.ShowAsync(this);
interaction.SetOutput(filename);
}
}
}
I've followed every step you gave me I thought
Kouhai
Kouhaiā€¢16mo ago
My bad, OnInitialized gets called before the DataContext is set to the ViewModel I guess you could go full ReactiveUI then
Mekasu0124
Mekasu0124OPā€¢16mo ago
ok so what do I need to do?
Kouhai
Kouhaiā€¢16mo ago
Change MainWindow declerate to this public partial class MainWindow : ReactiveWindow<MainWindowViewMode>
using Avalonia.Controls;
using Diary.ViewModels;
using ReactiveUI;
using System.Reactive;
using System.Threading.Tasks;

namespace Diary.Views
{
++ public partial class MainWindow : ReactiveWindow<MainWindowViewMode>`
-- public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
++ this.WhenActivated(d => d(ViewModel.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog)));
}
-- protected override void OnInitialized()
-- {
-- if (DataContext is MainWindowViewModel vm)
-- {
-- vm.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog);
-- }
-- }
private async Task ShowSaveFileDialog(InteractionContext<Unit, string?> interaction)
{
var dialog = new SaveFileDialog();
var filename = await dialog.ShowAsync(this);
interaction.SetOutput(filename);
}
}
}
using Avalonia.Controls;
using Diary.ViewModels;
using ReactiveUI;
using System.Reactive;
using System.Threading.Tasks;

namespace Diary.Views
{
++ public partial class MainWindow : ReactiveWindow<MainWindowViewMode>`
-- public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
++ this.WhenActivated(d => d(ViewModel.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog)));
}
-- protected override void OnInitialized()
-- {
-- if (DataContext is MainWindowViewModel vm)
-- {
-- vm.ShowSaveFileDialog.RegisterHandler(ShowSaveFileDialog);
-- }
-- }
private async Task ShowSaveFileDialog(InteractionContext<Unit, string?> interaction)
{
var dialog = new SaveFileDialog();
var filename = await dialog.ShowAsync(this);
interaction.SetOutput(filename);
}
}
}
What this would do is register the handler when the view gets activated So the view model would be set
Mekasu0124
Mekasu0124OPā€¢16mo ago
should this line say instead of this public partial class MainWindow : ReactiveWindow<MainWindowViewMode> it would be this public partial class MainWindow : ReactiveWindow<MainWindowViewModel>?
Kouhai
Kouhaiā€¢16mo ago
MainWindowViewModel šŸ˜…
Mekasu0124
Mekasu0124OPā€¢16mo ago
Mekasu0124
Mekasu0124OPā€¢16mo ago
Kouhai
Kouhaiā€¢16mo ago
Ignore the null ref warning for now What does the error say? It says the ReactiveUI<MainWindowViewModel> is less accessible than MainWindow I'll have to go now, good luck, you're basically 90% done <:MadoThumbsUp_MM:406514447973351444>
Mekasu0124
Mekasu0124OPā€¢16mo ago
90% done and have zero idea on how to fix this. Thanks for the help though
SinFluxx
SinFluxxā€¢16mo ago
Accessibility = public / private / internal etc
Accord
Accordā€¢16mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server