Alan2
Alan2

Reputation: 24572

How can I add in a template object into a C# Frame template

Here is code I have been working on with the help of Deczalof:

Here is the XAML code that I have

<t:PopupEntryFrame2 x:Name="newDeckNameEntry" TextChanged="newDeckNameEntry_TextChanged" />

With code behind:

public partial class CopyDeckPopup : Rg.Plugins.Popup.Pages.PopupPage
{
    string originalName;
    string originalDescription;
    List<Deck> listDecks;

    public CopyDeckPopup(string clickedDeckName, string clickedDeckDescription)
    {
        InitializeComponent();
        listDecks = App.DB.GetAllDecks();
        newDeckNameEntry.Text = clickedDeckName;
        newDeckDescriptionEntry.Text = clickedDeckDescription;
        originalName = clickedDeckName;
        originalDescription = clickedDeckDescription;
        OK_Button.IsEnabled = false;
    }

    private async void Cancel_Button_Clicked(object sender, EventArgs e)
    {
        await PopupNavigation.Instance.PopAsync(false);
    }

    private async void OK_Button_Clicked(object sender, EventArgs e)
    {
        if (IsBusy)
            return;
        IsBusy = true;
        await PopupNavigation.Instance.PopAsync(false);
        var newDeckNameEntryTextTrim = newDeckNameEntry.Text.Trim();
        var newDeckDescriptionEntryTextTrim = newDeckDescriptionEntry.Text.Trim();
        if (newDeckNameEntryTextTrim != originalName || newDeckDescriptionEntryTextTrim != originalDescription)
        {
            App.DB.CopyDeckToDb2(originalName, newDeckNameEntryTextTrim, newDeckDescriptionEntryTextTrim);
            MessagingCenter.Send<PopupPage>(new PopupPage(), "PageRefresh");
        }
        IsBusy = false;
    }

    void newDeckNameEntry_TextChanged(object sender, EntryTextChangedEventArgs e)
    {
        NewDeckNameEntryValidator(e.NewTextValue);
    }

    void newDeckDescriptionEntry_TextChanged(object sender, EntryTextChangedEventArgs e)
    {
        var deckName = newDeckNameEntry.Text.Trim();
        var isDeckAvailable = listDecks.Where(x => x.Name == deckName).SingleOrDefault();
        if (isDeckAvailable == null)
        {
            OK_Button.IsEnabled = e.NewTextValue != originalDescription ? true : false;
        }
    }

    void NewDeckNameEntryValidator(string newDeckNameEntry)
    {
        var newDeckNameEntryTrimmed = newDeckNameEntry.Trim();
        var isDeckNameAvailable = listDecks.Where(x => x.Name == newDeckNameEntryTrimmed).SingleOrDefault();
        if (string.IsNullOrWhiteSpace(newDeckNameEntryTrimmed) ||
            isDeckNameAvailable != null ||
            newDeckNameEntryTrimmed.StartsWith("::") ||
            newDeckNameEntryTrimmed.EndsWith("::") ||
            newDeckNameEntryTrimmed.Count(c => c == ':') > 2)
        {
            OK_Button.IsEnabled = false;
            return;
        }
        OK_Button.IsEnabled = true;
    }

}

and the C# code for a template:

public class PopupEntryFrame2 : CustomFrame
{

    CustomEntry entry { get; set; }

    public PopupEntryFrame2()
    {

        entry = new CustomEntry();
        entry.SetBinding(PopupEntryFrame2.TextProperty, new Binding("Text", source: this));

        entry.TextChanged += (s, a) =>
        {
            OnTextChanged(new EntryTextChangedEventArgs(a.NewTextValue, a.OldTextValue));
        };

        Content = entry;

        CornerRadius = 5;
        HasShadow = false;
        SetDynamicResource(BackgroundColorProperty, "EntryFrameBackgroundColor");
        SetDynamicResource(BorderColorProperty, "EntryFrameBorderColor");
        SetDynamicResource(CornerRadiusProperty, "EntryFrameCornerRadius");
        SetDynamicResource(HeightRequestProperty, "PopupEntryFrameHeight");
        SetDynamicResource(MarginProperty, "PopupEntryFrameMargin");
        SetDynamicResource(PaddingProperty, "PopupEntryFramePadding");
    }

    public class EntryTextChangedEventArgs : EventArgs
    {

        public EntryTextChangedEventArgs(String newValue = null, String oldValue = null)
        {
            NewTextValue = newValue;
            OldTextValue = oldValue;
        }

        public String NewTextValue { get; }

        public String OldTextValue { get; }

    }

    public event EventHandler TextChanged;


    protected virtual void OnTextChanged(EntryTextChangedEventArgs args)
    {
        TextChanged?.Invoke(this, args);
    }

    public static readonly BindableProperty TextProperty =
        BindableProperty.Create(nameof(Text), typeof(string), typeof(PopupEntryFrame2), default(string));

    public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }

}

The error I get when building is this:

CopyDeckPopup.xaml(22,63): XamlC error XFC0002: EventHandler "newDeckNameEntry_TextChanged" 
with correct signature not found in type "DecksTab.Pages.DeckOptions.CopyDeckPopup"

Upvotes: 3

Views: 364

Answers (1)

deczaloth
deczaloth

Reputation: 7485

To achieve your goal you can simply add the Entry on your PopupEntryFrame class and define an Event there that connects with the TextChanged event in the original Entry.

This is done as illustrated in the code below (which is based on yours!)

using Test.Renderers;

namespace Test.Templates
{
    public class PopupEntryFrame : CustomFrame
    {

        Entry entry { get; set; }

        public PopupEntryFrame()
        {

            entry = new Entry();

            entry.TextChanged += (s, a) =>
            {
                OnTextChanged(new EntryTextChangedEventArgs());
            };

            Content = entry;

            CornerRadius = 5;
            HasShadow = false;
            SetDynamicResource(BackgroundColorProperty, "EntryFrameBackgroundColor");
            SetDynamicResource(BorderColorProperty, "EntryFrameBorderColor");
            SetDynamicResource(CornerRadiusProperty, "EntryFrameCornerRadius");
            SetDynamicResource(HeightRequestProperty, "PopupEntryFrameHeight");
            SetDynamicResource(MarginProperty, "PopupEntryFrameMargin");
            SetDynamicResource(PaddingProperty, "PopupEntryFramePadding");
        }

        public class EntryTextChangedEventArgs : EventArgs
        {
            // class members  
        }

        public event EventHandler TextChanged;
        protected virtual void OnTextChanged(EntryTextChangedEventArgs args)
        {
            TextChanged?.Invoke(this, args);
        }

    }
}

And that's it. By doing that you can now write code like

<t:PopupEntry x:Name="newDeckDescriptionEntry" TextChanged="newDeckDescriptionEntry_TextChanged">

Update

In the comments someone suggested using ContentView, so let's take a look at how the same result could be achieved using that approach.

Disclaimer

First of all, it is important to know that Frame inherits itself from ContentView (from which acctualy it inherits its Content property!). In fact, from the documentation we know that

[Xamarin.Forms.ContentProperty("Content")]
[Xamarin.Forms.RenderWith(typeof(Xamarin.Forms.Platform._FrameRenderer))]
public class Frame : Xamarin.Forms.ContentView, Xamarin.Forms.IBorderElement, Xamarin.Forms.IElementConfiguration<Xamarin.Forms.Frame>

which means that by creating a Class/Control that inherits from Frame means that we are already using the ContentView approach.

Create the ContentView

First of all we create a ContentView and set its content to a new PopupFrame() which itself contains an Entry, as follows

public class PopupEntry : ContentView
{

    Entry entry { get; set; }

    public PopupEntry()
    {

        entry = new Entry();

        Content = new PopupFrame()
        {
            Content = entry
        };

    }

}

Add an Event

Next, as is required by the OP, we define an Event for our ContentView that will be triggered when the Text in the Entry changed. Following the Documentation, this can be achieved by adding the following piece of code:

public class EntryTextChangedEventArgs : EventArgs
{
    // class members  
}

public event EventHandler TextChanged;
protected virtual void OnTextChanged(EntryTextChangedEventArgs args)
{
    TextChanged?.Invoke(this, args);
}

Now, we can "link" the original TextChanged event from the Entry control to the new Event of our ContentView, as follows:

entry.TextChanged += (s, a) =>
{
    OnTextChanged(new EntryTextChangedEventArgs());
};

Then, our ContentView code will look like

public class PopupEntry : ContentView
{

    Entry entry { get; set; }

    public PopupEntry()
    {

        entry = new Entry();

        entry.TextChanged += (s, a) =>
        {
            OnTextChanged(new EntryTextChangedEventArgs());
        };

        Content = new PopupFrame()
        {
            Content = entry
        };


    }

    public class EntryTextChangedEventArgs : EventArgs
    {
        // class members  
    }

    public event EventHandler TextChanged;
    protected virtual void OnTextChanged(EntryTextChangedEventArgs args)
    {
        TextChanged?.Invoke(this, args);
    }

}

Wrapping up

With this ContentView defined, we can now write code like

<t:PopupEntry x:Name="newDeckDescriptionEntry" TextChanged="newDeckDescriptionEntry_TextChanged"/>

And that's it! I hope this was useful.

Happy coding!

P.S.:

A little note about the Event declaration: Since EntryTextChangedEventArgs is a copy of the original TextChangedEventArgs we can define the EntryTextChangedEventArgs class like

public class EntryTextChangedEventArgs : EventArgs
{

    public EntryTextChangedEventArgs(String newValue = null, String oldValue = null)
    {
        NewTextValue = newValue;
        OldTextValue = oldValue;
    }

    public String NewTextValue { get; }

    public String OldTextValue { get; }

}

and then when instantiating this class we just feed it directly with the values from TextChangedEventArgs, as follows

entry = new Entry();

entry.TextChanged += (s, a) =>
{
    OnTextChanged(new EntryTextChangedEventArgs(a.NewTextValue, a.OldTextValue));
};

Upvotes: 3

Related Questions