Shopware 6: Extend existing core DAL entities with a reference - shopware

I'm trying to create a connection between 2 existing entities PropertyGroup and CustomFieldSet. Use-case is irrelevant.
So I created an EntityExtension:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new ManyToOneAssociationField('customFieldSet', 'custom_field_set', CustomFieldSetDefinition::class))
);
}
public function getDefinitionClass(): string
{
return PropertyGroupDefinition::class;
}
And override the administration component to also include this association when loading the entity:
Component.override('sw-property-detail', {
methods: {
loadEntityData() {
this.isLoading = true;
const criteria = this.defaultCriteria;
criteria.addAssociation('customFieldSet', new Criteria(1, 500));
this.propertyRepository.get(this.groupId, Shopware.Context.api, criteria)
.then((currentGroup) => {
this.propertyGroup = currentGroup;
this.isLoading = false;
}).catch(() => {
this.isLoading = false;
});
}
}
});
(I tried to override defaultCriteria but that didn't work because of this.$super being unable to access computed properties).
But it keeps saying FRAMEWORK__ASSOCIATION_NOT_FOUND. I debugged the EntityDefinition and it seems that this extension is not even loaded.
I checked if my EntityExtension is loaded in the Symfony container and it is, but it seems that it doesn't reach the entity definition.

The EntityExtension seems to be missing the addition of a FkField inside the function extendFields:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new FkField('custom_field_set', 'customFieldSetId', CustomFieldSetDefinition::class)),
);
$collection->add(
(new ManyToOneAssociationField('customFieldSet', 'custom_field_set', CustomFieldSetDefinition::class))
);
}
A new use statement has to be added for the FkField:
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;

Related

Updating one-to-many relationships with Razor Pages

I cannot figure out how to update related data using Razor Pages asp.net core 2.0.
In the below example, I can update the "Project" data, but I cannot see the related "Action" data (one project => many actions) on OnPostAsync().
EditModel : PageModel
[BindProperty]
public Project Project { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
//THIS PART WORKS, I CAN SEE ACTIONS WITH THE PROJECT
Project = await _context.Project.Include(a=>a.Action).SingleOrDefaultAsync(m => m.Id == id);
if (Project == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
//MODEL IS NOT RETURNING ACTIONS (One project many actions)
if (!ModelState.IsValid)
{
return Page();
}
//How do I update actions?
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProjectExists(Project.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
OnModelCreating:
modelBuilder.Entity<Action>(entity =>
{
entity.HasOne(d => d.Project)
.WithMany(p => p.Action)
.HasForeignKey(d => d.ProjectId)
.HasConstraintName("FK_Action_Project");
});
I think you have not understood how BindProperty works. By using BindProperty for a property you ask from the framework the value of the property to be available in the view, and in case that you have an input containing the value it will go back to the property when you submit the contents of the input.
This means that even for the project object you will not get values if you do not have inputs which will host the values of the mode.
You have two options. The first option is to put inputs for all the your action items of the list which is not a good idea and the second option is to query the list from the database again after the submition of the form.

ASP.NET Core 1 Web API Model Binding Array

How do you model bind an array from the URI with GET in ASP.NET Core 1 Web API (implicitly or explicitly)?
In ASP.NET Web API pre Core 1, this worked:
[HttpGet]
public void Method([FromUri] IEnumerable<int> ints) { ... }
How do you do this in ASP.NET Web API Core 1 (aka ASP.NET 5 aka ASP.NET vNext)? The docs have nothing.
The FromUriAttribute class combines the FromRouteAttribute and FromQueryAttribute classes. Depending the configuration of your routes / the request being sent, you should be able to replace your attribute with one of those.
However, there is a shim available which will give you the FromUriAttribute class. Install the "Microsoft.AspNet.Mvc.WebApiCompatShim" NuGet package through the package explorer, or add it directly to your project.json file:
"dependencies": {
"Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
}
While it is a little old, I've found that this article does a pretty good job of explaining some of the changes.
Binding
If you're looking to bind comma separated values for the array ("/api/values?ints=1,2,3"), you will need a custom binder just as before. This is an adapted version of Mrchief's solution for use in ASP.NET Core.
public class CommaDelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.IsEnumerableType)
{
var key = bindingContext.ModelName;
var value = bindingContext.ValueProvider.GetValue(key).ToString();
if (!string.IsNullOrWhiteSpace(value))
{
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(x.Trim()))
.ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
// change this line to null if you prefer nulls to empty arrays
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0));
}
return TaskCache.CompletedTask;
}
return TaskCache.CompletedTask;
}
}
You can either specify the model binder to be used for all collections in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc().AddMvcOptions(opts =>
{
opts.ModelBinders.Insert(0, new CommaDelimitedArrayModelBinder());
});
}
Or specify it once in your API call:
[HttpGet]
public void Method([ModelBinder(BinderType = typeof(CommaDelimitedArrayModelBinder))] IEnumerable<int> ints)
ASP.NET Core 1.1 Answer
#WillRay's answer is a little outdated. I have written an 'IModelBinder' and 'IModelBinderProvider'. The first can be used with the [ModelBinder(BinderType = typeof(DelimitedArrayModelBinder))] attribute, while the second can be used to apply the model binder globally as I've show below.
.AddMvc(options =>
{
// Add to global model binders so you don't need to use the [ModelBinder] attribute.
var arrayModelBinderProvider = options.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
options.ModelBinderProviders.Insert(
options.ModelBinderProviders.IndexOf(arrayModelBinderProvider),
new DelimitedArrayModelBinderProvider());
})
public class DelimitedArrayModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsEnumerableType && !context.Metadata.ElementMetadata.IsComplexType)
{
return new DelimitedArrayModelBinder();
}
return null;
}
}
public class DelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
var values = valueProviderResult
.ToString()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
if (values.Length == 0)
{
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(elementType, 0));
}
else
{
var converter = TypeDescriptor.GetConverter(elementType);
var typedArray = Array.CreateInstance(elementType, values.Length);
try
{
for (int i = 0; i < values.Length; ++i)
{
var value = values[i];
var convertedValue = converter.ConvertFromString(value);
typedArray.SetValue(convertedValue, i);
}
}
catch (Exception exception)
{
bindingContext.ModelState.TryAddModelError(
modelName,
exception,
bindingContext.ModelMetadata);
}
bindingContext.Result = ModelBindingResult.Success(typedArray);
}
return Task.CompletedTask;
}
}
There are some changes in the .NET Core 3.
Microsoft has split out the functionality from the AddMvc method (source).
As AddMvc also includes support for View Controllers, Razor Views and etc. If you don't need to use them in your project (like in an API), you might consider using services.AddControllers() which is for Web API controllers.
So, updated code will look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddMvcOptions(opt =>
{
var mbp = opt.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
opt.ModelBinderProviders.Insert(opt.ModelBinderProviders.IndexOf(mbp), new DelimitedArrayModelBinderProvider());
});
}

Specific TableController name not working

I have an extremely odd error and wondered if anyone knew the reason for this.
When I create a new DataObject and TableController called Content and ContentController respectively, it doesn't register the tablecontroller and the help documentation it automatically generates has lost its styling.
I can't connect to the controller at all but all other controllers work as expected.
If I just rename it to DataController and that's just the name of the controller, not the dataobject everything works perfectly.
Is ContentController a reserved word of some kind or is this just specifically happening on my machine?
public class DataController : TableController<Content>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MobileContext context = new MobileContext();
DomainManager = new EntityDomainManager<Content>(context, Request, Services);
}
// GET tables/Content
public IQueryable<Content> GetAllContent()
{
return Query();
}
// GET tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<Content> GetContent(string id)
{
return Lookup(id);
}
// PATCH tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<Content> PatchContent(string id, Delta<Content> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public async Task<IHttpActionResult> PostContent(Content item)
{
Content current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteContent(string id)
{
return DeleteAsync(id);
}
}
An MVC project will create an application directory called Content. This will override your route mapping to the ContentController.
You can get around this if desired through changing RouteMaps and other trickery although probably the simpliest answer is to change the name of the controller...

How do you Bind an Image to the HighlightedImage-Property of an ImageView?

I'm currently trying to bind two images to an iOS ImageView via MvvmCross.
One should be displayed when the ImageView is in 'default' state, the other one when the ImageView is highlighted.
By the following code I can bind the Image for the default state. But how do I bind the one for 'highlighted' state?
public CategoryCell(IntPtr handle): base(string.Empty, handle)
{
_imageViewLoader = new MvxImageViewLoader(() => this.imageView);
this.DelayBind(() =>
{
var set = this.CreateBindingSet<CategoryCell, MaterialCategory>();
set.Bind(titleLabel).To(materialCategory => materialCategory.Label);
set.Bind(_imageViewLoader).To(materialCategory => materialCategory.ImageActiveUri);
set.Bind(this).For(cell => cell.Selected).To(materialCategory => materialCategory.IsSelected);
set.Apply();
});
}
Another approach if you do not need image loading i.e. for lots of static UX.
You can set up as follows -
_imageView = new UIImageView(UIImage.FromFile("some/image/off.png"))
{
HighlightedImage = UIImage.FromFile("some/image/on.png")
};
And bind it e.g. an "Enabled" Property -
this.DelayBind(() =>
{
var set = this.CreateBindingSet<SomeView, SomeViewModel>();
set.Bind(_imageView).For(v => v.Highlighted).To(vm => vm.Enabled);
set.Apply();
});
And don't forget to add Highlighted to your LinkerPleaseInclude.cs.
Hope this helps
I think the best solution is to introduce an extra property ImageUri. In the setter of your IsSelected you set the ImageUri dependend on the selection state.
ViewModel:
public class MaterialCategory : MvxViewModel
{
//...
public string ImageActiveUri { ... } // call UpdateImageUri() here, too
public string ImageInactiveUri { ... } // call UpdateImageUri() here, too
public string ImageUri { ... }
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
UpdateImageUri();
RaisePropertyChanged(() => IsSelected);
}
}
private void UpdateImageUri()
{
ImageUri = IsSelected ? ImageActiveUri : ImageInactiveUri;
}
}
Binding:
set.Bind(_imageViewLoader).To(materialCategory => materialCategory.ImageUri);
// instead of:
// set.Bind(_imageViewLoader).To(materialCategory => materialCategory.ImageActiveUri);

How can I get this old Ninject 2 code to work with Ninject 3 and the MVC 2 extension (NinjectControllerFactory)?

In my MVC 2 project, I originally used Ninject 2 and wrote this version of the NinjectControllerFactory:
public class NinjectControllerFactory : DefaultControllerFactory
{
private IKernel kernel = new StandardKernel(new HandiGamerServices());
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
try
{
if (controllerType == null)
{
return base.GetControllerInstance(requestContext, controllerType);
// return null;
}
}
catch (HttpException ex)
{
if (ex.GetHttpCode() == 404)
{
IController errorController = kernel.Get<ErrorController>();
((ErrorController)errorController).InvokeHttp404(requestContext.HttpContext);
return errorController;
}
else
{
throw ex;
}
}
return (IController)kernel.Get(controllerType);
}
Of most importance is the retrieval of my ErrorController, which allows me to gracefully handle a multitude of HTTP errors.
The problem is that I upgraded to the MVC 2 extension via Nuget, so a NinjectControllerFactory is already provided. Would it be possible to use my own override of GetControllerInstance? If so, how?
I do exactly this, and for precisely the same reason. In Global.asax.cs, I add this to my OnApplicationStarted override (declared virtual in NinjectHttpApplication):
ControllerBuilder.Current.SetControllerFactory(
new MyControllerFactory(ControllerBuilder.Current.GetControllerFactory()));
This means you're creating your own controller factory, but providing it with the default implementation to do the heavy lifting.
Then define your controller factory like so:
public class MyControllerFactory : IControllerFactory
{
private IControllerFactory defaultFactory;
public MyControllerFactory(IControllerFactory defaultFactory)
{
this.defaultFactory = defaultFactory;
}
public IController CreateController(RequestContext requestContext, string controllerName)
{
try
{
var controller = defaultFactory.CreateController(requestContext, controllerName);
return controller;
}
catch (HttpException e)
{
// Pasted in your exception handling code here:
if (ex.GetHttpCode() == 404)
{
IController errorController = kernel.Get<ErrorController>();
((ErrorController)errorController).InvokeHttp404(requestContext.HttpContext);
return errorController;
}
else
{
throw ex;
}
}
}
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return defaultFactory.GetControllerSessionBehavior(requestContext, controllerName);
}
public void ReleaseController(IController controller)
{
defaultFactory.ReleaseController(controller);
}
}
As you can see, we're just using the default (Ninject) controller factory for most purposes unless it can't find the page. For obtaining the error controller, you can either pass in the kernel as you were already doing, or just call defaultFactory.CreateController using the error controller name.

Resources