7

My code looks like this right now with two lines of code for each message. The code works but if I have for example 30 messages that I can each give values to then I will need to have 60 lines of code just to declare everything:

string _msg1;
string _msg2;
public string Msg1 { get => _msg1; set => SetProperty(ref _msg1, value); }
public string Msg2 { get => _msg2; set => SetProperty(ref _msg2, value); }

and in C# I assign to these:

vm.Msg1 = "A";
vm.Msg2 = "B"; 

and in the XAML I bind my Text to Msg1 and another Text to Msg2

Can someone tell me how / if I can do this with array so that I would assign like this and hopefully so the assignment of the array can just be done in two lines instead of 2 lines for every single message:

vm.Msg[0] = "A";
vm.Msg[1] = "B";

For reference:

public class ObservableObject : INotifyPropertyChanged
{

    protected virtual bool SetProperty<T>(
        ref T backingStore, T value,
        [CallerMemberName]string propertyName = "",
        Action onChanged = null)
    {
        if (EqualityComparer<T>.Default.Equals(backingStore, value))
            return false;

        backingStore = value;
        onChanged?.Invoke();
        OnPropertyChanged(propertyName);
        return true;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

}
Alan2
  • 23,493
  • 79
  • 256
  • 450
  • Just a reminder. For the bounty I am looking for an alternate answer to LadderLogic's so I can have something else to consider. If the alternate is better explained or seems more appropriate I will accept that one. – Alan2 Oct 25 '18 at 08:17
  • I am a little confused and need some clarification based on my understanding. The original model example has individual properties but then you mention something about arrays. I got lost with the jump from the individual properties to arrays. So are you asking if you can access those properties the same way you would with an array? – Nkosi Oct 25 '18 at 09:38
  • Currently I am declaring all of the messages like this, public string Msg1 and public string Msg1. But what if I have 30 messages Msg1, Msg2 .. Msg30. I would like to be able to declare these with just an array and not have to repeat "public string Msg1" and "public string Msg1 { get => _msg1; set => SetProperty(ref _msg1, value); }" 30 times. – Alan2 Oct 25 '18 at 14:49
  • The answer given mentions "// INotifyPropertyChanged and SetProperty implementation goes here" but I have already given my SetProperty code so I am wondering where this fits in. What I would like is an answer that's nicely formatted and that would show me (and others) just the lines of code that would be needed. I think maybe 90% of the work has been done but the existing answer doesn't look very clear to me so I am hoping for something that I can try cutting and pasting into my code (or other people can cut paste) that will do what's needed. Hope this makes sense. – Alan2 Oct 25 '18 at 14:53
  • Yes this clarifies most of my concerns / confusion. – Nkosi Oct 25 '18 at 14:56
  • Their `BindableValue` and your `ObservableObject` are practically the same except that their own is generic. In fact you can create their by just inheriting your `ObservableObject` (ie `BindableValue : ObservableObject { ... }`) – Nkosi Oct 25 '18 at 15:24
  • Assume, msgArray[30] is availbale. How do you set values to the array, and to what control you will bind it to in the UI. (eg: ListView, Buttons, textblocks). – Vibeeshan Mahadeva Oct 26 '18 at 09:57
  • In other words is it ok if we show all the messages as a list in the view. (Like, a List view showing all the 30 items (Strings) with a vertical scrollbar) – Vibeeshan Mahadeva Oct 26 '18 at 10:00

5 Answers5

8

You can create a simple wrapper class with indexing that supports property change notification.

For example:

public class Messages : ObservableObject
{
    readonly IDictionary<int, string> _messages = new Dictionary<int, string>();

    [IndexerName("Item")] //not exactly needed as this is the default
    public string this[int index]
    {
        get
        {
            if (_messages.ContainsKey(index))
                return _messages[index];

//Uncomment this if you want exceptions for bad indexes
//#if DEBUG
//          throw new IndexOutOfRangeException();
//#else
            return null; //RELEASE: don't throw exception
//#endif
        }

        set
        {
            _messages[index] = value;
            OnPropertyChanged("Item[" + index + "]");
        }
    }
}

And, create a property in view model as:

private Messages _msg;
public Messages Msg
{
    get { return _msg ?? (_msg = new Messages()); }
    set { SetProperty(ref _msg, value); }
}

Now you can set or update values as:

vm.Msg[0] = "A";
vm.Msg[1] = "B";

Bindings in XAML will be same as:

<Label Text="{Binding Msg[0]}" />
<Label Text="{Binding Msg[1]}" />

Sample usage code

XAML

<StackLayout Margin="20">
    <Label Text="{Binding Msg[0]}" />
    <Label Text="{Binding Msg[1]}" />
    <Label Text="{Binding Msg[2]}" />
    <Label Text="{Binding Msg[3]}" />
    <Label Text="{Binding Msg[4]}" />

    <Button Text="Trigger update" Command="{Binding UpdateMessage}" />
</StackLayout>

Code-behind, view-model

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        var viewModel = new MainViewModel();

        viewModel.Msg[0] = "Original message 1";
        viewModel.Msg[1] = "Original message 2";
        viewModel.Msg[2] = "Original message 3";
        viewModel.Msg[3] = "Original message 4";
        viewModel.Msg[4] = "Original message 5";

        BindingContext = viewModel;
    }
}

public class MainViewModel : ObservableObject
{
    private Messages _msg;
    public Messages Msg
    {
        get { return _msg ?? (_msg = new Messages()); }
        set { SetProperty(ref _msg, value); }
    }

    public ICommand UpdateMessage => new Command(() =>
           {
               Msg[2] = "New message 3";
               Msg[0] = "New message 1";
           });
}

enter image description here

Sharada Gururaj
  • 13,471
  • 1
  • 22
  • 50
  • Is this possible to use in ASP.NET MVC 4, or not because there is no Observable Object (I don't seem to be able to find an assembly that has it) - is that a Xamarin thing? – Dronz Apr 21 '20 at 06:05
7

Arrays will not raise property changed event. You'll need to use an ObservableCollection that can raise an event when the collection has changed. However, this doesn't raise an event when the object inside the collection has changed it's value. You'll need to wrap your object, in this case a string, into a type that can raise property changed events.

Something like the following would work:

    public class BindableValue<T> : INotifyPropertyChanged
    {
        private T _value;
        public T Value
        { get => _value; set => SetProperty(ref _value, value); }
        // INotifyPropertyChanged and SetProperty implementation goes here
    }

    private ObservableCollection<BindableValue<string>> _msg;
    public ObservableCollection<BindableValue<string>> Msg
    { get => _msg; set => SetProperty(ref _msg1, value); }

you would be binding to Msg[0].Value, Msg[1].Value etc.,

LadderLogic
  • 1,090
  • 9
  • 17
  • Can you show how this could be used using the SetProperty method that's in the question so I can get a better understanding of what you mean. Also can you show if I can use the get => and set => shortcuts. Thanks – Alan2 Oct 23 '18 at 00:55
  • 3
    edited with SetProperty. I'd recommend implementing INotifyPropertyChanged in the base class and reusing them instead of re-writing. Or, if you're still at the infancy of the project, I recommend looking up an existing MVVM framework that does this for you. One of my favorites is the Prism. – LadderLogic Oct 23 '18 at 19:20
  • Can you explain (or add to the code), what you mean by // INotifyPropertyChanged and SetProperty implementation goes here } Thanks – Alan2 Oct 25 '18 at 05:33
4

Not entirely sure that I got the question, but as I understood the simplest way is this:

The Viewmodel:

Just bind to an ObservableCollection of strings, because it already implements INotifyCollectionChanged and INotifyPropertyChanged. RelayCommand is just an implementation of ICommand and I'm assuming you have heard of them since you are doing WPF MVVM.

using System.Collections.ObjectModel;

namespace WpfApp1
{
    public class MainWindowViewmodel
    {
        public ObservableCollection<string> Messages { get; set; }
        public MainWindowViewmodel()
        {
            Messages = new ObservableCollection<string>();
            Messages.Add("My message!");
            ChangeMessageCommand = new RelayCommand(ChangeMessageExcecute); 
        }
        public RelayCommand ChangeMessageCommand { get; set; }
        private void ChangeMessageExcecute() => Messages[0] = "NEW message!";
    }
}

The View:

In the view you can just bind your Textblocks to the Elements of the ObservableCollection. When you press the button, the Command gets called and changes the message in the window.

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>
            <TextBlock Text="{Binding Messages[0]}" HorizontalAlignment="Center"/>
            <Button Content="Change Message" Command="{Binding ChangeMessageCommand}" Width="200"/>
        </StackPanel>
    </Grid>
</Window>

Kind regards, misdirection

3

I Assume that your given example is running and working as expected (Atleast with 2 items)

View Code.

Assuming you want to show all the 30 messages as a list.

<ListView ItemsSource="{Binding MessagesArray}"/>

Also you should set the DataContext properly, Comment below if you need any help

View Model Code.

We are using an ObservableCollection instead of array. Since pure arrays doesn't support proper binding features.

private ObservableCollection<string> _messagesArray;

public ObservableCollection<string> MessagesArray  
{  
    get { return _messagesArray; }  
    set { SetProperty(ref _messagesArray, value); }  
}  

Assigning Values

MessagesArray = new ObservableCollection<string>();
vm.MessagesArray.Add("A");
vm.MessagesArray.Add("B");

In the assignment code MessagesArray = new ObservableCollection<string>(); assigns a new object of ObservableCollection of String

If you are new to ObservableCollection think of this as an wrapper to string[], but not actually true

SetProperty method will tell the XAML View that a new collection is arrived, so the UI will rerender the list.

When you call vm.MessagesArray.Add("B"); internal logics inside the method Add will tell the XAML View a new item is added to the ObservableCollection so the view can rerender the ListView with the new item.

Update 27 October 2018

You can create your own array using any of the below ways. (Not all)

 string[] dataArray = new string[30];

1. this will create an array with 30 null values

 string[] dataArray = { "A", "B", "C" }; //Go up to 30 items

2. this will create an array with predefined set of values, you can go up to 30

 string[] dataArray = Enumerable.Repeat<string>(String.Empty, 30).ToArray();

3. this will create an array with string which holds empty values, Instead of String.Empty you can put any string value.

Choose any of the above method

I recommend the last method, then you can assign that into a Observable Collection like below.

MessagesArray = new ObservableCollection<string>(dataArray); 

Now the trick is

vm.MessagesArray[0] = "A"
vm.MessagesArray[25] = "Z"

View might look like below

<TextBlock Text="{Binding MessagesArray[0]}"/>
<TextBlock Text="{Binding MessagesArray[1]}"/>
Vibeeshan Mahadeva
  • 7,147
  • 8
  • 52
  • 102
  • 1
    Hi, I don't want to show the array as a list. I have a XAML page where I am using vm.Msg[0] in one place, vm.Msg[1] in another place etc. I don't want to call "vm.MessagesArray.Add("B");" I want to be able to assign a value of "B" to an element in an array. For example vm.Msg[25] = "BBB"; – Alan2 Oct 27 '18 at 05:00
  • @Alan2 Can you show the XAML Code, because only after knowing how do you want to presnt the data we can help you. What is the control you are binding to.. if you are binding it to textblock or label then even in XAML you have write the code 30 time, Please explain more – Vibeeshan Mahadeva Oct 27 '18 at 07:44
  • Once the array has been created and if I have added vm.MessagesArray[25] = "Z", then can I later change it to vm.MessagesArray[25] = "ZZZZ" and have what appears on the screen change? This is what I am looking for. – Alan2 Oct 28 '18 at 01:42
  • @Alan2 check the new updated answer, mentioned below "Update 27 October 2018", yest if you set vm.MessagesArray[25] = "Your new value it will update automatically" – Vibeeshan Mahadeva Oct 28 '18 at 03:54
2

What about using reflection? You can ask for all the public properties of type string with name "Msg*".

For example:

static class Program
{
    static void Main(string[] args)
    {
        var vm = new MessagesViewModel();

        PropertyInfo[] myProperties = vm.GetType()
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => p.PropertyType == typeof(string) && p.Name.Contains("Msg"))
            .ToArray();

        foreach (var propertyInfo in myProperties)
        {
            //You can also access directly using the indexer --> myProperties[0]..
            propertyInfo.SetValue(vm, $"This is {propertyInfo.Name} property");
        }

        Console.WriteLine(vm.Msg1);
        Console.WriteLine(vm.Msg2);
    }
}

public class MessagesViewModel
{
    string _msg1;
    string _msg2;
    public string Msg1 { get => _msg1; set => _msg1 = value; }
    public string Msg2 { get => _msg2; set => _msg2 = value; }
}

If this type of solution fits, you can wrap it with an indexer, sort the array to match the index and the Msg[num].

Arye
  • 381
  • 2
  • 7