Search code examples
c#xamlmvvmdata-bindingvisual-studio-2022

Is it possible to do two way data binding in XAML directly with objects generated by Visual Studio REST Client service code generator?


Is it possible or practical to hookup 2-way data binding in XAML directly to a Visual Studio generated RESTApi based on OpenApi metadata file. For instance, Visual Studio generates a REST client, based on an OpenApi file or metadata published at a URL.

MyRESTClient _client = new MyRESTClient("http://localhost", new System.Net.Http.HttpClient());

That would maybe appear at the top of some ViewModel. Let's assume we are working with a relative REST endpoint like:

/api/v1/Kitten

The implementation of the Get method which takes no parameters returns all Kittens. Visual Studio RESTClient generator will generate a complete REST API for objects available at all endpoints. So now assuming the namespace is the same as the application, we have a Kittens object.

I can define in the ViewModel, a Kittens object named _kittens like this:

private ObservableCollection<Kittens> _kittens;

I can then retrieve all kittens and convert the returned ICollection that came from the REST call into an ObservableCollection like this:

_kittens = new ObservableCollection<Kitten>(await _client.KittenAllAsync());

Cool, now I have a two way data bound collection of kitten objects and the UI can do whatever. I can also register, with the new ObservableCollection the event handlers I need in order to respond to add/remove/move events in the collection, and issue the corresponding REST calls to update the records on the remote data store.

My question is this:

Is it possible to also receive events about changes within the fields of any given kitten so I can also execute the corresponding REST calls to update just the one kitten, or better yet, just the one property of that one kitten?


Solution

  • You could also bind WPF to the server in the form of MVVM. The following describes how to bind in the form of MVVM.

    Assuming your server is an ASP.NET Core Web API project. Create HomeController in Controllers and modify the code as follows.

    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        // Data in the server to be synchronized with WPF
        List<Kittens> list = new List<Kittens>() { new Kittens() { Id = "1",Name= "Kittens_1" },new Kittens() { Id = "2", Name = "Kittens_2" } };
    
        [HttpGet("GetData")]
        public List<Kittens> GetData()
        {
            return list;
        }
    
        [HttpPost("UpdateData")]
        public string UpdateData(Kittens kittens)
        {
            //Find the data to be modified by Id in the List and modify it
    
            return  "Modified data"+kittens.Id + ":" + kittens.Name;
        }
    }
    
    public class Kittens
    {
        public string Id { get; set; }
    
        public string Name { get; set; }
    }
    

    Add the following code in Program.cs to allow cross-domain communication. Then WPF could communicate with the server through HttpClient.

    Add the following code below var builder = WebApplication.CreateBuilder(args);

    builder.Services.AddCors(p =>
    {
        p.AddPolicy("MyCors", builder => {
            builder.AllowAnyHeader();
            builder.AllowAnyMethod();
            builder.AllowAnyOrigin();
        });
    });
    

    Add the following code below var app = builder.Build();

    app.UseCors(p =>
    {
        p.AllowAnyHeader();
        p.AllowAnyMethod();
        p.AllowAnyOrigin();
    });
    

    Assume that your WPF project is WPF Application. First, you need to install CommunityToolkit.Mvvm and Newtonsoft.Json through NuGet.

    Create a Model folder and create Kittens in the Model

        public class Kittens:ObservableObject
        {
            public string id;
    
            public string Id { get { return id; } set { id = value;OnPropertyChanged("Id"); } }
    
            public string name;
    
            public string Name { get { return name; } set { name = value;OnPropertyChanged("Name"); } }
        }
    

    Create a ViewModel folder and create MainWindowViewModel in the ViewModel folder.

    Complete code

    public class MainWindowViewModel:ObservableObject 
    {
        public ObservableCollection<Kittens> kittens { get; set; }
    
        public RelayCommand<Kittens> saveCommand { get; set; }
    
        private static readonly HttpClient client = new HttpClient();
    
        private DispatcherTimer dispatcherTimer;
    
        public MainWindowViewModel() {
            kittens = new ObservableCollection<Kittens>();
            saveCommand = new RelayCommand<Kittens>(SaveData);
    
            LoadData();
            InitializeTimer();
        }
    
        public async Task LoadData()
        {
            var response = await client.GetAsync("…/Home/GetData");
    
            var responseString = await response.Content.ReadAsStringAsync();
            var list = JsonConvert.DeserializeObject<List<Kittens>>(responseString);
            kittens.Clear();
            foreach (var item in list)
            {
                kittens.Add(item);
            }
        }
    
        public void SaveData(Kittens kittens)
        {
            string json = JsonConvert.SerializeObject(kittens);
            HttpContent content =new StringContent(json, Encoding.UTF8, "application/json");
            client.PostAsync("…/Home/UpdateData", content);
        }
    
        private void InitializeTimer()
        {
            dispatcherTimer = new DispatcherTimer();
            dispatcherTimer.Interval = TimeSpan.FromSeconds(60*5); // Set the timer interval in seconds
            dispatcherTimer.Tick += DispatcherTimer_Tick;
            dispatcherTimer.Start();
        }
    
        private void DispatcherTimer_Tick(object sender, EventArgs e)
        {
            // Perform timing operations here
            LoadData();
        }
    }
    

    In the above code, LoadData is used to get data from the server, and then kittens could pass the data to DataGrid through data binding. Here, the DispatcherTimer timer in InitializeTimer is used to intermittently execute LoadData to achieve intermittent synchronization from the server to WPF (do not set the interval too small, as sending network requests all the time is very resource-consuming)

    Through Command, when the Save Edit button is clicked, the SaveData method is executed, and a network request is sent to the server to update the data in the server, thereby realizing synchronization from WPF to the server.

    MainWindow.xaml

    xmlns:vm="clr-namespace:WpfApp.ViewModel"
    
    <Window.Resources>
        <vm:MainWindowViewModel x:Key="MainWindowViewModel"></vm:MainWindowViewModel>
    </Window.Resources>
    <Grid DataContext="{StaticResource MainWindowViewModel}">
        <DataGrid x:Name="MyDataGrid" ItemsSource="{Binding kittens}" IsReadOnly="False" AutoGenerateColumns="False" CanUserAddRows="False" >
            <DataGrid.Columns>
                <DataGridTextColumn Header="Id" Binding="{Binding Id,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
                <DataGridTextColumn Header="Name" Binding="{Binding Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
                <DataGridTemplateColumn Header="Option">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid DataContext="{StaticResource MainWindowViewModel}">
                                <Button Content="Save Edit"  Command="{Binding saveCommand}" 
                                CommandParameter="{Binding Path=CurrentItem,RelativeSource={RelativeSource AncestorType=DataGrid,AncestorLevel=1,Mode=FindAncestor}}"></Button>
                            </Grid>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>