Why does not Xamarin bind property? - c#

I want to save data to application on window closing or application crashes.
When user writes to entry the data gets storen in property, but for some reason the binding does not work.
I followed a course on Udemy for this. I think it has something to do with referencing to different place in PCL.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace TheIVInventory.ViewModels
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class AddItemPage : ContentPage
{
public AddItemPage()
{
InitializeComponent();
BindingContext = Application.Current;
}
private void Button_Clicked(object sender, EventArgs e) //Item added click.
{
}
}
}
Xaml :
<ContentPage
BackgroundColor="#104850"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="TheIVInventory.ViewModels.AddItemPage">
<StackLayout VerticalOptions="Center" x:Name="formLayout" Margin="20">
<Entry PlaceholderColor="White" Keyboard="Chat" Margin="40" Placeholder="Item Name" TextColor="White" Text="{Binding ItemName}"></Entry>
<Entry PlaceholderColor="White" Keyboard="Numeric" Margin="40" Placeholder="Item Price MIN (€)" TextColor="White"></Entry>
<Entry PlaceholderColor="White" Keyboard="Numeric" Margin="40" Placeholder="Item Price MAX (€)" TextColor="White"></Entry>
<Editor PlaceholderColor="White" Margin="40" VerticalOptions="FillAndExpand" Keyboard="Chat" Placeholder="Item Description" TextColor="White"></Editor>
<Button Text="Save" BackgroundColor="#80EEFF" Margin="10" Clicked="Button_Clicked" ></Button>
<Image Source="konjakki.png" Scale="0.15" AnchorY="0" BackgroundColor="#104850" ></Image>
</StackLayout>
</ContentPage>
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace TheIVInventory
{
public partial class App : Application
{
// Setting the item add members.
private const string itemNameKey = "Name";
private const string itemMinPrice = "0";
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage())
{
BarBackgroundColor = Color.FromHex("#104850"),
BarTextColor = Color.White
}; // Making the navigation possible.
}
protected override void OnStart()
{
// Handle when your app starts
}
protected override void OnSleep()
{
// Handle when your app sleeps
}
protected override void OnResume()
{
// Handle when your app resumes
}
// Making the add item properties.
public string ItemName
{
get
{
if (Properties.ContainsKey(itemNameKey))
return Properties[itemNameKey].ToString();
return "";
}
set
{
Properties[ItemName] = value;
}
}
public string ItemMinPrice
{
get
{
if (Properties.ContainsKey(itemMinPrice))
return Properties[itemMinPrice].ToString();
return "";
}
set
{
Properties[itemMinPrice] = value;
}
}
}
}

The main issue is that you don't appear to have set a BindingContext for your XAML to refer to.
It also looks to me as if you are trying to implement MVVM structure, but have not entirely understood it.
The points that immediately draw my eye are:
Your view code is within a ViewModel namespace rather than View
(which will prove confusing later on).
You have code that should be within a ViewModel class (which will be your Views BindingContext) in your main App code.
I would suggest that you create a Views namespace and move your AddItemPage code to it;
Create an AddItemViewModel and use it to implement your ItemName and ItemMinPrice properties.
Instead of using a button_clicked event in the code behind, bind the Button Command to an ICommand property type in your ViewModel. Then have your ViewModel instantiate the ICommand to use an internal method to run your save code.

If you want to achieve the MVVM in xamarin forms. Your binding way is wrong, you could create a model, put the ItemMinPrice and ItemName in this model like following code.Achieve the INotifyPropertyChanged interface, when data was changed.
class MyModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
string itemMinPrice;
public string ItemMinPrice
{
set
{
if (itemMinPrice != value)
{
itemMinPrice = value;
OnPropertyChanged("ItemMinPrice");
}
}
get
{
return itemMinPrice;
}
}
string itemName;
public string ItemName
{
set
{
if (itemName != value)
{
itemName = value;
OnPropertyChanged("ItemName");
}
}
get
{
return itemName;
}
}
}
Then you can change the bindingContext, like this code BindingContext = new MyModel();
I add a break point in the MyModel, give a itemName in the Entry you can see it was executed like following GIF.
Note: If you want to achieve that model data changed could display the view. You should change Mode to TwoWay like following code.
<Entry PlaceholderColor="White" Keyboard="Chat" Margin="40"
Placeholder="Item Name" TextColor="Black" Text="{Binding ItemName,
Mode=TwoWay}"></Entry>
Here is offical artical about MVVM, you can refer to it.
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/xaml/xaml-basics/data-bindings-to-mvvm

Related

How do I trigger a data refresh on my FlyoutContentTemplate view model when navigating back from my add page?

I am building a Xamarin Forms application using Shell in which I am not building your typical flyout. I have created a template each for the header, footer and content. The content contains data which is fetched from a database.
I'm defining these in my app.xaml file.
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="FlyoutFooterTemplate" Value="{DataTemplate common:FlyoutFooterTemplate}"/>
<Setter Property="FlyoutHeaderTemplate" Value="{DataTemplate common:FlyoutHeaderTemplate}"/>
<Setter Property="FlyoutContentTemplate" Value="{DataTemplate common:FlyoutContentTemplate}"/>
</Style>
I have created the FlyoutContentTemplate as a RefreshView.
<?xml version="1.0" encoding="utf-8" ?>
<RefreshView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:InFlight.ViewModels.Common"
xmlns:model="clr-namespace:InFlight.Core.Models;assembly=InFlight.Core"
x:Class="InFlight.Views.Common.FlyoutContentTemplate"
x:DataType="local:FlyoutContentTemplateViewModel"
Command="{Binding LoadFlightsCommand}"
IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
<CollectionView
ItemsSource="{Binding Flights}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:Flight">
<Label Text="{Binding CallSign}"
LineBreakMode="NoWrap"
Style="{StaticResource LabelMedium}" />
<Label Text="{Binding FlightNotes}"
LineBreakMode="NoWrap"
Style="{StaticResource LabelSmall}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.Footer>
<Button Command="{Binding AddFlightCommand}" Text="Add Flight" />
</CollectionView.Footer>
</CollectionView>
</RefreshView>
The code behind simply sets the BindingContext.
using InFlight.ViewModels.Common;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace InFlight.Views.Common
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FlyoutContentTemplate : RefreshView
{
readonly FlyoutContentTemplateViewModel viewModel;
public FlyoutContentTemplate()
{
InitializeComponent();
this.BindingContext = viewModel = new FlyoutContentTemplateViewModel();
}
}
}
The view model is fairly simple and handles the LoadFlightsCommand triggered by the RefreshView and the navigation to the AddEditFlightPage.
using InFlight.Core.Models;
using InFlight.Core.Respositories;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace InFlight.ViewModels.Common
{
public class FlyoutContentTemplateViewModel : BaseViewModel
{
public ObservableCollection<Flight> Flights { get; set; }
public Command LoadFlightsCommand { get; }
public Command AddFlightCommand { get; }
readonly IFlightRepository flightRepository;
public FlyoutContentTemplateViewModel()
{
flightRepository = DependencyService.Get<IFlightRepository>();
Flights = new ObservableCollection<Flight>();
LoadFlightsCommand = new Command(ExecuteLoadFlightsCommand);
AddFlightCommand = new Command(async () => await OnAddFlightCommand());
ExecuteLoadFlightsCommand();
}
private async Task OnAddFlightCommand()
{
await Shell.Current.GoToAsync("AddEditFlightPage");
Shell.Current.FlyoutIsPresented = false;
}
private void ExecuteLoadFlightsCommand()
{
IsBusy = true;
try
{
Flights.Clear();
var flts = flightRepository.GetFlights();
foreach (var flight in flts)
{
Flights.Add(flight);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
}
This all seems to work well, but at the moment I need to pull to refresh in order to trigger the LoadFlightsCommand.
The issue is that I want to trigger the data refresh when navigating back from my add page. I've seen posts where people tap into the OnAppearing event in order to trigger the refresh, but as I am using a template and refresh view, I don't see how I can do that.
Have I maybe taken the wrong approach and using Shell for a purpose it shouldn't be used for?
I'm thinking that the solution probably involves the events of the shell itself?
Any advice would be much appreciated.
First Approach
I don't have the full picture on how you are defining your Shell's routes/navigation, but I believe what you are trying to achieve could be done using Shell event OnNavigated or OnNavigating.
First thing, Shell (AppShell.xaml.cs) need to have access to the instance of your FlyoutContentTemplateViewModel in roder to call the method ExecuteLoadFlightsCommand() from there.
Turn the accessibility of ExecuteLoadFlightsCommand() to public.
AppShell.xaml.cs
public FlyoutContentTemplateViewModel flyoutContentTemplateViewModel;
public AppShell()
{
flyoutContentTemplateViewModel = new();
InitializeComponent();
}
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
var previousRouteString = args?.Previous?.Location?.OriginalString;
var currentRouteString = args?.Current?.Location?.OriginalString;
if (previousRouteString != null && previousRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]") &&
currentRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]"))
{
flyoutContentTemplate.ExecuteLoadFlightsCommand();
}
base.OnNavigated(args);
}
In your FlyoutContentTemplate(), use the same ViewModel instance from the field that we have added in your AppShell.
public FlyoutContentTemplate()
{
InitializeComponent();
BindingContext = viewModel = (Shell.Current as AppShell).flyoutContentTemplateViewModel;
}
Second Approach
If you don't want to store your VM in your AppShell then you might use DependencyService.
Extract an interface from your FlyoutContentTemplateViewModel: on visual studio select the class name, right click, in the menu click "Quick Actions and refactoring", after that click "Extract interface", VS will generate an interface called IFlyoutContentTemplateViewModel:
public interface IFlyoutContentTemplateViewModel
{
Command AddFlightCommand { get; }
ObservableCollection<Flight> Flights { get; set; }
bool IsBusy { get; }
Command LoadFlightsCommand { get; }
Task OnAddFlightCommand()
void ExecuteLoadFlightsCommand();
}
FlyoutContentTemplate.xaml.cs
public FlyoutContentTemplate()
{
InitializeComponent();
BindingContext = viewModel = new FlyoutContentTemplateViewModel();
DependencyService.RegisterSingleton<IFlyoutContentTemplateViewModel>(viewModel);
}
AppShell.xaml.cs
...
if (previousRouteString != null && previousRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]") &&
currentRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]"))
{
DependencyService.Resolve<IFlyoutContentTemplateViewModel>()?.ExecuteLoadFlightsCommand();
}
Third Approach
Calling ExecuteLoadFlightsCommand() from OnDisappearing() of AddEditFlightPage, instead of AppShell.OnNavigated().
AddEditFlightPage.xaml.cs
protected override void OnDisappearing()
{
(DependencyService.Resolve<IFlyoutContentTemplateViewModel>())?.ExecuteLoadFlightsCommand();
base.OnDisappearing();
}

xamarin forms : can't bind data

I'm trying to create an app which retrieves data from my api but I just don't know why the binding doesn't work. Here's the C# code
public partial class PageData : ContentPage
{
TodoList tdl { get; set; }
public PageData()
{
tdl = new TodoList();
this.BindingContext = tdl;
InitializeComponent();
}
private async void ContentPage_Appearing(object sender, EventArgs e)
{
var httpClientHandler = new HttpClientHandler();
httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; };
HttpClient client = new HttpClient(httpClientHandler);
var WebAPIUrl = #"http://192.168.x.xx:65xxx/api/data";
var uri = new Uri(WebAPIUrl);
var response = await client.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
string responseBody = await response.Content.ReadAsStringAsync();
var tmp = JsonConvert.DeserializeObject<List<TodoItem>>(responseBody);
tdl.todoLists = new System.Collections.ObjectModel.ObservableCollection<TodoItem>(tmp);
}
// MyListView.ItemsSource = tdl.todoLists;
}
}
It works when I use that last line I commented but it kind of feel like "cheating" as this isn't the best practice when using MVVM. I know there's a way around that but I just dont know what I'm doing wrong. thanks.
and here is the xaml code :
<ContentPage.Content>
<ListView x:Name="MyListView" ItemsSource="{Binding todoLists}" RowHeight="70">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" Spacing="20" Padding="5">
<StackLayout Orientation="Vertical">
<Label Text="To do: " FontAttributes="Bold" FontSize="17"></Label>
<Label Text="Is completed: " FontAttributes="Bold" FontSize="17"></Label>
</StackLayout>
<StackLayout Orientation="Vertical">
<Label Text="{Binding name}" FontSize="17"></Label>
<Label Text="{Binding isCompleted}" FontSize="17"></Label>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
here's my MVVM class :
using ExamenAout.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
namespace ExamenAout.MVVM
{
public class TodoList : INotifyPropertyChanged
{
private ObservableCollection<TodoItem> _todoLists { get; set; }
public ObservableCollection<TodoItem> todoLists
{
get { return this._todoLists; }
set
{
this._todoLists = value;
OnPropertyRaised("todoList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyRaised(string PropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
}
}
here's the TodoItem class :
using System;
using System.Collections.Generic;
using System.Text;
namespace ExamenAout.Models
{
public class TodoItem
{
public Guid userid { get; set; }
public string name { get; set; }
public bool isCompleted { get; set; }
}
}
Next time you can use nameof expression to prevent this mistake:) :
public ObservableCollection<TodoItem> todoLists
{
get { return this._todoLists; }
set
{
this._todoLists = value;
OnPropertyRaised(nameof(todoLists));
}
}
To expand on Jack Hua's answer, you could also use CallerMemberNameAttribute. As the documentation explains:
Implementing the INotifyPropertyChanged interface when binding data. This interface allows the property of an object to notify a bound control that the property has changed, so that the control can display the updated information. Without the CallerMemberName attribute, you must specify the property name as a literal.
You can use it as such:
using System.Runtime.CompilerServices;
private void OnPropertyRaised([CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
This makes it so the property's name you're setting is automatically passed
public ObservableCollection<TodoItem> todoLists
{
get => _todoLists;
set
{
_todoLists = value;
OnPropertyRaised(); // You don't need to pass "todoLists" here
}
}

ListView ItemsSource binding not displaying items

I am trying to create a bound ListView in Xamarin. Here's the C# code:
public partial class LearnPage : ContentPage
{
public class TodoItem
{
public string DisplayName { get; set; }
}
//ObservableCollection<TodoItem> Items = new ObservableCollection<TodoItem>();
private IEnumerable<TodoItem> _items;
public IEnumerable<TodoItem> Items
{
get { return _items; }
set
{
if (Equals(_items, value))
return;
_items = value;
OnPropertyChanged();
}
}
public LearnPage ()
{
InitializeComponent();
BindingContext = this;
Items = new TodoItem[]{
new TodoItem{ DisplayName = "Milk cartons are recyclable" }
};
//Items.Add(new TodoItem { DisplayName = "Milk cartons are recyclable" });
}
}
You can also see some commented out code with an ObervableCollection, which I have also tried with.
And here's the XAML:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="learn.LearnPage">
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" Padding="0,10,0,10">
<ListView ItemsSource="{Binding Items}" RowHeight="40" x:Name="sarasas">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding DisplayName}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
When I build the app, an empty list is displayed. I'm really new to Xamarin and I think I'm just missing something obvious. Any help appreciated!
I am not sure if ContentPage uses [CallerMemberName] for the OnPropertyChanged() method. So first thing I would try is to write OnPropertyChanged("Items") instead.
Either way, if I were you I would separate concerns and move the Items into a ViewModel class, which implements INotifyPropertyChanged itself. This will help later on if you want to test, add more code such as commands, inject dependencies etc., where you will keep ViewModel code separate from View code.
So you could start with:
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged ([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (propertyName));
}
}
Then create your ViewModel:
public class LearnViewModel : BaseViewModel
{
public ObservableCollection<TodoItem> Items { get; } = new ObservableCollection<TodoItem>();
private ICommand _addTodoItem;
public ICommand AddTodoItem =>
_addTodoItem = _addTodoItem ?? new Command(DoAddTodoItem);
private int _count;
private void DoAddTodoItem()
{
var item = new TodoItem { DisplayName = $"Item {++_count}" };
// NotifyCollectionChanged must happen on UI thread.
Device.BeginInvokeOnMainThread (() => {
Items.Add(item);
});
}
}
Then you can keep your View code thin like:
public partial class LearnPage : ContentPage
{
public LearnPage ()
{
InitializeComponent();
BindingContext = new LearnViewModel();
}
}
Normally you would invoke a command to populate the Items in your ViewModel, this could be by fetching data from the Internet, or loading local data from a database etc.
In this sample you can just add a constructor to the LearnViewModel and call DoAddTodoItem a couple of times:
public class LearnViewModel : BaseViewModel
{
public LearnViewModel()
{
DoAddTodoItem();
DoAddTodoItem();
}
...
This should show you something like this when you launch the app:

WPF databinding: view not updating from viewmodel

My WPF app is working in a strange way for me - some binding works, other not.
I have following situation:
A textbox - user provides an ID. Based on this ID an object is loaded or created. Some other properties are updated by values coming from the loaded/new object.
Binding for the ID textbox works fine. However, two other views (any other) not.
My code samples:
XAML:
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBlock Text="ID" FontFamily="Segoe UI Light" />
<TextBox x:Name="TB_PacientID" Width="100px" HorizontalAlignment="Left" Margin="5,0,0,0" Text="{Binding Path=PacientID}"/>
<TextBlock x:Name="TBL_NovyPacient" Text="nový pacient" Margin="5,0,0,0" Foreground="Green" FontWeight="Bold" Visibility="{Binding Path=IsNewPacient,UpdateSourceTrigger=PropertyChanged,Converter={StaticResource BTVConverter}}"/>
</StackPanel>
<WrapPanel x:Name="WP_PacientData" Margin="-2,5,2,5" Visibility="{Binding PacientLoaded,Converter={StaticResource BTVConverter}}">
...
Viewmodel:
public int? PacientID
{
get
{
if (CurrentPacient == null)
return null;
return CurrentPacient.id;
}
set
{
if (value != null)
{
_pacient = App.instance.sessionData.serviceProxy.pacientById(value.Value);
if (_pacient == null)
{
CurrentPacient = new Pacient() { id = value.Value };
IsNewPacient = true;
}
else
{
CurrentPacient = _pacient;
}
OnPropertyChanged();
PacientLoaded = true;
}
}
}
// ...
public bool IsNewPacient
{
get{ return _isNewPacient; }
set
{
_isNewPacient = value;
OnPropertyChanged();
}
}
//...
public bool PacientLoaded
{
get{ return _pacientLoaded; }
set
{
_pacientLoaded = value;
OnPropertyChanged();
}
}
The idea:
User inputs the ID, an object is loaded or created and the WrapPanel is shown. If the object is newly created the TextBlock is shown as well.
The converters are working fine (tested in another window).
When the window loads, the binding is established well (if I set some fake values in ctor). When changing the ID in textbox, nothing other updates - except for the ID itself - the setter is fired well and the new value is read after OnPropertyChanged is called.
I hope I'm missing something very easy and stupid.
-Edit:
Current state:
TB_PacientID is working (updading), TBL_NovyPacient and WP_PacientData not working (updating).
I want:
All thee views updating from viewmodel (the code properties).
-Edit 2
I created a very simple example of my problem from scratch:
A window - two textboxes:
<Window x:Class="bindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<TextBox x:Name="TestTextBox" Text="{Binding ID, Mode=TwoWay}"/>
<TextBox x:Name="SecondTextBox" Text="{Binding IsNew, Mode=TwoWay}"/>
</StackPanel>
</Window>
Codebehind:
namespace bindingTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new TestViewModel();
}
}
}
And the viewmodel class:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace bindingTest
{
public abstract class ViewModelBase
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class TestViewModel : ViewModelBase
{
private bool _attOne;
private int? id;
private bool _isNew;
public bool IsNew
{
get
{
return _isNew;
}
set
{
_isNew = value;
OnPropertyChanged();
}
}
public int? ID
{
get
{
return id;
}
set
{
this.id = value;
IsNew = true;
OnPropertyChanged();
}
}
}
}
And what I simply want - If I change the number in the first textbox I want to have True in the second textbox automatically.
Yes, I am stupid.
My ViewModel base class lost the INotifyPropertyChanged interface while merging from another project.
So I called the OnPropertyChanged, but it has been my own OnPropertyChanged instead of implementation of the interface which is WPF binding waiting for.
I had threethings to point in your code sample:
You should use a TwoWay binding for setting the ID.
Are you sure the _pacient = App.instance.sessionData.serviceProxy.pacientById(value.Value); code returns always the same object instance.
Are you correctly using the INotifyPropertyChanged interface in most cases you raising a property change events looks like this: RaisePropertyChanged('PropertyName'); you are invoking: 'OnPropertyChanged();'
Hope this helps...

Making AvalonEdit MVVM compatible

I'm trying to make Avalon MVVM compatible in my WPF application. From googling, I found out that AvalonEdit is not MVVM friendly and I need to export the state of AvalonEdit by making a class derived from TextEditor then adding the necessary dependency properties. I'm afraid that I'm quite lost in Herr Grunwald's answer here:
If you really need to export the state of the editor using MVVM, then I suggest you create a class deriving from TextEditor which adds the necessary dependency properties and synchronizes them with the actual properties in AvalonEdit.
Does anyone have an example or have good suggestions on how to achieve this?
Herr Grunwald is talking about wrapping the TextEditor properties with dependency properties, so that you can bind to them. The basic idea is like this (using the CaretOffset property for example):
Modified TextEditor class
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(MvvmTextEditor),
// binding changed callback: set value of underlying property
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.CaretOffset = (int)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
public new int CaretOffset
{
get { return base.CaretOffset; }
set { base.CaretOffset = value; }
}
public int Length { get { return base.Text.Length; } }
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Now that the CaretOffset has been wrapped in a DependencyProperty, you can bind it to a property, say Offset in your View Model. For illustration, bind a Slider control's value to the same View Model property Offset, and see that when you move the Slider, the Avalon editor's cursor position gets updated:
Test XAML
<Window x:Class="AvalonDemo.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:avalonExt="clr-namespace:WpfTest.AvalonExt"
DataContext="{Binding RelativeSource={RelativeSource Self},Path=ViewModel}">
<StackPanel>
<avalonExt:MvvmTextEditor Text="Hello World" CaretOffset="{Binding Offset}" x:Name="editor" />
<Slider Minimum="0" Maximum="{Binding ElementName=editor,Path=Length,Mode=OneWay}"
Value="{Binding Offset}" />
<TextBlock Text="{Binding Path=Offset,StringFormat='Caret Position is {0}'}" />
<TextBlock Text="{Binding Path=Length,ElementName=editor,StringFormat='Length is {0}'}" />
</StackPanel>
</Window>
Test Code-behind
namespace AvalonDemo
{
public partial class TestWindow : Window
{
public AvalonTestModel ViewModel { get; set; }
public TestWindow()
{
ViewModel = new AvalonTestModel();
InitializeComponent();
}
}
}
Test View Model
public class AvalonTestModel : INotifyPropertyChanged
{
private int _offset;
public int Offset
{
get { return _offset; }
set
{
_offset = value;
RaisePropertyChanged("Offset");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You can use the Document property from the editor and bind it to a property of your ViewModel.
Here is the code for the view :
<Window x:Class="AvalonEditIntegration.UI.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
Title="Window1"
WindowStartupLocation="CenterScreen"
Width="500"
Height="500">
<DockPanel>
<Button Content="Show code"
Command="{Binding ShowCode}"
Height="50"
DockPanel.Dock="Bottom" />
<AvalonEdit:TextEditor ShowLineNumbers="True"
Document="{Binding Path=Document}"
FontFamily="Consolas"
FontSize="10pt" />
</DockPanel>
</Window>
And the code for the ViewModel :
namespace AvalonEditIntegration.UI
{
using System.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
public class ViewModel
{
public ViewModel()
{
ShowCode = new DelegatingCommand(Show);
Document = new TextDocument();
}
public ICommand ShowCode { get; private set; }
public TextDocument Document { get; set; }
private void Show()
{
MessageBox.Show(Document.Text);
}
}
}
source : blog nawrem.reverse
Not sure if this fits your needs, but I found a way to access all the "important" components of the TextEditor on a ViewModel while having it displayed on a View, still exploring the possibilities though.
What I did was instead of instantiating the TextEditor on the View and then binding the many properties that I will need, I created a Content Control and bound its content to a TextEditor instance that I create in the ViewModel.
View:
<ContentControl Content="{Binding AvalonEditor}" />
ViewModel:
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
// ...
private TextEditor m_AvalonEditor = new TextEditor();
public TextEditor AvalonEditor => m_AvalonEditor;
Test code in the ViewModel (works!)
// tests with the main component
m_AvalonEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
m_AvalonEditor.ShowLineNumbers = true;
m_AvalonEditor.Load(#"C:\testfile.xml");
// test with Options
m_AvalonEditor.Options.HighlightCurrentLine = true;
// test with Text Area
m_AvalonEditor.TextArea.Opacity = 0.5;
// test with Document
m_AvalonEditor.Document.Text += "bla";
At the moment I am still deciding exactly what I need my application to configure/do with the textEditor but from these tests it seems I can change any property from it while keeping a MVVM approach.

Categories

Resources