WoistdasNiveau
WoistdasNiveau

Reputation: 83

Execute Method in Custom View from ViewModel

I am highly confused on this. I have created a Custom SKCanvasView with some additional logic for moving Images and especially a CropImage() method. The confusion now is, how can i execute this Method from a Binding in the ViewModel containing this Custom SKCanvasView? I tried creating a BindableProperty of type bool to execute the Method when the bool is set to true but this did not quite work and seemed fishy. I also tried creating an ICommand but as the Microsoft Documentation (https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/commanding?view=net-maui-7.0) does not show how to create Custom Commands i am very confused on that too. How can i now Execute the CropMap() Method in my Custom SkCanvasView from the ViewModel assigned to the View which implements this Custom View?

Custom View:

public class CroppingCanvasView : SKCanvasView, INotifyPropertyChanged
    {
        // == Properties ==
        public SKBitmap Bitmap
        {
            get
            {
                return (SKBitmap)GetValue(BitmapProperty);
            }
            set
            {
                SetValue(BitmapProperty, value);
            }
        }

        SKRect croppingRect;
        SKRect CroppingRect
        {
            get
            {
                return croppingRect;
            }
            set
            {
                SKRect rect = Matrix.Invert().MapRect(value);
                WeakReferenceMessenger.Default.Send(new CroppingRectChangedMessage(rect));
                croppingRect = rect;
            }
        }

        SKMatrix Matrix { get; set; }

        SKRect BitmapRect { get; set; }
        public SKBitmap CropBitmap { get; set; }
        public byte[] CroppedImage
        {
            get
            {
                return (byte[])GetValue(CroppedImageProperty);
            }
            set
            {
                SetValue(CroppedImageProperty, value);
            }
        }

        // == Bindable Properties ==
        public static readonly BindableProperty BitmapProperty =
            BindableProperty.Create(nameof(Bitmap), typeof(SKBitmap), typeof(CroppingCanvasView));

        public static readonly BindableProperty CroppedImageProperty =
            BindableProperty.Create(nameof(CroppedImage), typeof(byte[]), typeof(CroppingCanvasView),
                defaultBindingMode: BindingMode.OneWayToSource);

        // == Command Properties ==
        public ICommand CropImage
        {
            get
            {
                return (ICommand)GetValue(CropImageProperty);
            }
            set
            {
                SetValue(CropImageProperty, value);
            }
        }

        // == Commands ==
        public static readonly BindableProperty CropImageProperty =
            BindableProperty.Create(nameof(CropImage), typeof(ICommand), typeof(CroppingCanvasView));

        // == fields ==
        TouchEffect touchEffect;
        Dictionary<long, SKPoint> touchDictionary;
        public CroppingCanvasView()
        {
            Matrix = SKMatrix.CreateIdentity();
            touchEffect = new TouchEffect();
            touchDictionary = new Dictionary<long, SKPoint>();
        }


        protected override void OnPropertyChanged([CallerMemberName] string propertyName = nameof(Bitmap))
        {
            base.OnPropertyChanged(propertyName);
            if (Bitmap != null)
            {
                BitmapRect = SKRect.Create(0,0,Bitmap.Width,Bitmap.Height);
                touchEffect.TouchAction += TouchEffect_TouchAction;
            }
        }

        // == override Methods ==
        protected override void OnPaintSurface(SkiaSharp.Views.Maui.SKPaintSurfaceEventArgs e)
        {
            base.OnPaintSurface(e);

            SKImageInfo info = e.Info;
            SKSurface surface = e.Surface;
            SKCanvas canvas = surface.Canvas;
            canvas.Clear();

            // Display the bitmap
            Matrix = CalculateMatrix(Matrix);
            canvas.SetMatrix(Matrix);
            canvas.DrawBitmap(Bitmap, new SKPoint(0,0));
            CroppingRect = SKRect.Create(0, 0, CanvasSize.Width, CanvasSize.Height);
        }

        protected override void OnParentSet()
        {
            base.OnParentSet();

            // Attach TouchEffect to parent view
            touchEffect.Capture = true;
            Parent.Effects.Add(touchEffect);
        }

        // == private Methods ==
        private void TouchEffect_TouchAction(object sender, Maui.FreakyEffects.TouchTracking.TouchActionEventArgs e)
        {
            Maui.FreakyEffects.TouchTracking.TouchTrackingPoint pt = e.Location;
            // Convert Xamarin.Forms point to pixels
            SKPoint point =
            new SKPoint((float)(CanvasSize.Width * pt.X / Width),
            (float)(CanvasSize.Height * pt.Y / Height));

            switch (e.Type)
            {
                case TouchActionType.Pressed:
                    // Find transformed bitmap rectangle
                    SKRect rect = new SKRect(0, 0, Bitmap.Width, Bitmap.Height);
                    rect = Matrix.MapRect(rect);

                    // Determine if the touch was within that rectangle
                    if (rect.Contains(point) && !touchDictionary.ContainsKey(e.Id))
                    {
                        touchDictionary.Add(e.Id, point);
                    }
                    break;

                case TouchActionType.Moved:
                    if (touchDictionary.ContainsKey(e.Id))
                    {
                        // Single-finger drag
                        if (touchDictionary.Count == 1)
                        {
                            SKPoint prevPoint = touchDictionary[e.Id];

                            // Adjust the matrix for the new position
                            SKMatrix matrix = Matrix;
                            matrix.TransX += point.X - prevPoint.X;
                            matrix.TransY += point.Y - prevPoint.Y;
                            matrix = CalculateMatrix(matrix);
                            Matrix = matrix;
                            InvalidateSurface();
                        }
                        // Double-finger scale and drag
                        else if (touchDictionary.Count == 2)
                        {
                            // Copy two dictionary keys into array
                            long[] keys = new long[touchDictionary.Count];
                            touchDictionary.Keys.CopyTo(keys, 0);

                            // Find index of non-moving (pivot) finger
                            int pivotIndex = (keys[0] == e.Id) ? 1 : 0;

                            // Get the three points involved in the transform
                            SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
                            SKPoint prevPoint = touchDictionary[e.Id];
                            SKPoint newPoint = point;

                            // Calculate two vectors
                            SKPoint oldVector = prevPoint - pivotPoint;
                            SKPoint newVector = newPoint - pivotPoint;
                            //SKPoint oldVector = prevPoint - new SKPoint(canvasView.CanvasSize.Width/2, (canvasView.CanvasSize.Width/2);

                            float scale = newVector.LengthSquared / oldVector.LengthSquared;

                            // Scaling factors are ratios of those
                            float scaleX = newVector.X / oldVector.X;
                            float scaleY = newVector.Y / oldVector.Y;

                            if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
                                !float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
                            {
                                // If something bad hasn't happened, calculate a scale and translation matrix
                                SKMatrix scaleMatrix = SKMatrix.CreateScale(scale, scale, pivotPoint.X, pivotPoint.Y);
                                SKMatrix matrix = Matrix;
                                matrix = matrix.PostConcat(scaleMatrix);
                                matrix = CalculateMatrix(matrix);
                                Matrix = matrix;
                                InvalidateSurface();
                            }
                        }

                        // Store the new point in the dictionary
                        touchDictionary[e.Id] = point;
                    }

                    break;

                case TouchActionType.Released:
                case TouchActionType.Cancelled:
                    if (touchDictionary.ContainsKey(e.Id))
                    {
                        touchDictionary.Remove(e.Id);
                        CropMap();       
                    }
                    break;
            }


        }

        private SKMatrix CalculateMatrix(SKMatrix getMatrix)
        {
            SKMatrix matrix = getMatrix;
            if (matrix.MapRect(BitmapRect).Size.Width < CanvasSize.Width ||
                matrix.MapRect(BitmapRect).Size.Height < CanvasSize.Height)
            {
                float scaleX = CanvasSize.Width / Bitmap.Width;
                float scaleY = CanvasSize.Height / Bitmap.Height;

                if (matrix.MapRect(BitmapRect).Size.Width == matrix.MapRect(BitmapRect).Size.Height)
                {
                    matrix.ScaleX = scaleX;
                    matrix.ScaleY = scaleY;
                }
                else if (matrix.MapRect(BitmapRect).Size.Width < matrix.MapRect(BitmapRect).Size.Height)
                {
                    matrix.ScaleX = scaleX;
                    matrix.ScaleY = scaleX;
                }
                else if (matrix.MapRect(BitmapRect).Size.Width > matrix.MapRect(BitmapRect).Size.Height)
                {
                    matrix.ScaleX = scaleY;
                    matrix.ScaleY = scaleY;
                }
            }
            if (matrix.MapRect(BitmapRect).Left > 0)
            {
                matrix.TransX -= matrix.MapRect(BitmapRect).Left;
            }
            if(matrix.MapRect(BitmapRect).Size.Height < CanvasSize.Height)
            {
                matrix.ScaleY = Bitmap.Height / CanvasSize.Height;
            }
            if (matrix.MapRect(BitmapRect).Left > 0)
            {
                matrix.TransX -= matrix.MapRect(BitmapRect).Left;
            }
            if (matrix.MapRect(BitmapRect).Right < CanvasSize.Width)
            {
                matrix.TransX += -matrix.MapRect(BitmapRect).Right + CanvasSize.Width;
            }
            if (matrix.MapRect(BitmapRect).Top > 0)
            {
                matrix.TransY -= matrix.MapRect(BitmapRect).Top;
            }
            if (matrix.MapRect(BitmapRect).Bottom < CanvasSize.Height)
            {
                matrix.TransY += -matrix.MapRect(BitmapRect).Bottom + CanvasSize.Height;
            }
            return matrix;
        }

        private void CropMap()
        {
            SKBitmap destination = new SKBitmap(1080, 1080);
            SKRect destRect = SKRect.Create(0, 0, 1080, 1080);
            using (SKCanvas canvas = new SKCanvas(destination))
            {
                canvas.Clear();
                canvas.DrawBitmap(Bitmap, CroppingRect, destRect);
            }

            SKImage skImage = SKImage.FromBitmap(destination);
            SKData encoded = skImage.Encode(SKEncodedImageFormat.Jpeg, 100);
            using (MemoryStream memory = new MemoryStream())
            {
                encoded.AsStream().CopyTo(memory);
                CroppedImage = null;
                CroppedImage = memory.ToArray();
            }
        }

Implementing View:


    <ContentPage.Resources>
        <ResourceDictionary>
            <toolkit:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageSourceConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Test" Command="{Binding CropCommand}"/>
    </ContentPage.ToolbarItems>
    
    <Grid x:Name="test">
        <Grid.RowDefinitions>
            <RowDefinition Height="0.5*"/>
            <RowDefinition Height="0.5*"/>
        </Grid.RowDefinitions>
        <local:CroppingCanvasView Grid.Row="0" Bitmap="{Binding Map, Mode=OneWay}" CroppedImage="{Binding CroppedImage}" CropImage="{Binding ClickCommand}"
                                  BackgroundColor="Aqua" VerticalOptions="CenterAndExpand"/>
        <VerticalStackLayout Grid.Row="1">
            <Image Source="{Binding CroppedImage, Converter={StaticResource ByteArrayToImageSourceConverter}}" Grid.Row="1"
               MaximumHeightRequest="200" MaximumWidthRequest="200" Margin="50"/>
        </VerticalStackLayout>
    </Grid>

The Code behind just sets the BindignContext as a new CutImagesViewModel.

ViewModel:

public partial class CutImagesViewModel : ObservableObject
    {
        [ObservableProperty]
        public ObservableCollection<UploadImageModel> uploadImages;

        [ObservableProperty]
        public SKBitmap map;

        [ObservableProperty]
        public byte[] croppedImage;

        partial void OnCroppedImageChanged(byte[] value)
        {
            Console.WriteLine("cropped Image changed");
        }

        public CutImagesViewModel(ObservableCollection<UploadImageModel> files)
        {
            UploadImages = new ObservableCollection<UploadImageModel>();
            UploadImages = files;
            Map = UploadImages[0].Image;
        }

        [RelayCommand]
        public void Click()
        {
            Console.WriteLine("clicked");
        }

        [RelayCommand]
        public void Crop()
        {
            Click();
            Console.WriteLine("crop");
        }
    }

Edit 1: What i especially do not understand is when i create a Bindable property of type bool like this:

public bool CropImage
        {
            get
            {
                return (bool)GetValue(CropImageProperty);
            }
            set
            {
                SetValue(CropImageProperty, value);
            }
        }

public static readonly BindableProperty CropImageProperty =
            BindableProperty.Create(nameof(CropImage), typeof(bool), typeof(CroppingCanvasView)
                , propertyChanged: (bindable, oldValue, newValue) =>
                {
                    Console.WriteLine("prop changes");
                } );

And Bind it in the Viewmodel with periodically changing the bool value to the opposite:

[ObservableProperty]
        public bool crop;

while(true)
            {
                if(Crop)
                    Crop = false;
                
                else
                    Crop = true;
                Thread.Sleep(1000);
            }

and set a Breakpoint to the get as well as the set of the Property weather the get nor the set is ever called even thoug the Bindign is set

<local:CroppingCanvasView Grid.Row="0" Bitmap="{Binding Map, Mode=OneWay}" CroppedImage="{Binding CroppedImage}" CropImage="{Binding Crop}"
                                  BackgroundColor="Aqua" VerticalOptions="CenterAndExpand"/>

and the get of the Bitmap gets called correctly as expected.

Upvotes: 0

Views: 116

Answers (1)

WoistdasNiveau
WoistdasNiveau

Reputation: 83

Obviously the PropertyChanged event will not get called when the constructor of the ViewModel is not finished. When i create a Button to change the Value of the bool custom Bindable Property so that i only changes when the Class is created completely it works fine.

Upvotes: 0

Related Questions